489 lines
10 KiB
C
489 lines
10 KiB
C
/* $NetBSD: pw_policy.c,v 1.9 2006/03/19 22:58:21 elad Exp $ */
|
|
|
|
/*-
|
|
* Copyright 2005, 2006 Elad Efrat <elad@NetBSD.org>
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Neither the name of The NetBSD Foundation nor the names of its
|
|
* contributors may be used to endorse or promote products derived
|
|
* from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
|
|
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <util.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <limits.h>
|
|
#include <ctype.h>
|
|
#include <unistd.h>
|
|
#include <err.h>
|
|
#include <errno.h>
|
|
#include <pwd.h>
|
|
#include <grp.h>
|
|
#include <assert.h>
|
|
|
|
#include <machine/int_limits.h>
|
|
|
|
#define PW_POLICY_DEFAULTKEY "pw_policy"
|
|
|
|
#define LOAD_POLICY 0
|
|
#define TEST_POLICY 1
|
|
|
|
#define MINMAX_ERR(min, max, n) ((min == 0 || max == 0) && n != 0) || \
|
|
(min > 0 && min > n) || \
|
|
(max > 0 && max < n)
|
|
|
|
#define HANDLER_PROTO pw_policy_t, int, char *, void *, void *
|
|
#define HANDLER_ARGS pw_policy_t policy, int flag, char *pw, void *arg, void *arg2
|
|
|
|
static int pw_policy_parse_num(char *, int32_t *);
|
|
static int pw_policy_parse_range(char *, int32_t *, int32_t *);
|
|
|
|
static int pw_policy_handle_len(HANDLER_PROTO);
|
|
static int pw_policy_handle_charclass(HANDLER_PROTO);
|
|
static int pw_policy_handle_nclasses(HANDLER_PROTO);
|
|
static int pw_policy_handle_ntoggles(HANDLER_PROTO);
|
|
|
|
struct pw_policy {
|
|
int32_t minlen;
|
|
int32_t maxlen;
|
|
int32_t minupper;
|
|
int32_t maxupper;
|
|
int32_t minlower;
|
|
int32_t maxlower;
|
|
int32_t mindigits;
|
|
int32_t maxdigits;
|
|
int32_t minpunct;
|
|
int32_t maxpunct;
|
|
int32_t mintoggles;
|
|
int32_t maxtoggles;
|
|
int32_t minclasses;
|
|
int32_t maxclasses;
|
|
};
|
|
|
|
struct pw_policy_handler {
|
|
const char *name;
|
|
int (*handler)(HANDLER_PROTO);
|
|
void *arg2;
|
|
};
|
|
|
|
static struct pw_policy_handler handlers[] = {
|
|
{ "length", pw_policy_handle_len, NULL },
|
|
{ "lowercase", pw_policy_handle_charclass, islower },
|
|
{ "uppercase", pw_policy_handle_charclass, isupper },
|
|
{ "digits", pw_policy_handle_charclass, isdigit },
|
|
{ "punctuation", pw_policy_handle_charclass, ispunct },
|
|
{ "nclasses", pw_policy_handle_nclasses, NULL },
|
|
{ "ntoggles", pw_policy_handle_ntoggles, NULL },
|
|
{ NULL, NULL, NULL },
|
|
};
|
|
|
|
static int
|
|
pw_policy_parse_num(char *s, int32_t *n)
|
|
{
|
|
char *endptr = NULL;
|
|
long l;
|
|
|
|
if (s[0] == '*' && s[1] == '\0') {
|
|
*n = -1;
|
|
return 0;
|
|
}
|
|
|
|
errno = 0;
|
|
l = strtol(s, &endptr, 10);
|
|
if (*endptr != '\0')
|
|
return EINVAL;
|
|
if (errno == ERANGE && (l == LONG_MIN || l == LONG_MAX))
|
|
return ERANGE;
|
|
|
|
if (l < 0 || l >= UINT16_MAX)
|
|
return ERANGE;
|
|
|
|
*n = (int32_t)l;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
pw_policy_parse_range(char *range, int32_t *n, int32_t *m)
|
|
{
|
|
char *p;
|
|
|
|
while (isspace((unsigned char)*range))
|
|
range++;
|
|
p = &range[strlen(range) - 1];
|
|
while (isspace((unsigned char)*p))
|
|
*p-- = '\0';
|
|
|
|
/* Single characters: * = any number, 0 = none. */
|
|
if (range[0] == '0' && range[1] == '\0') {
|
|
*n = *m = 0;
|
|
return 0;
|
|
}
|
|
if (range[0] == '*' && range[1] == '\0') {
|
|
*n = *m = -1;
|
|
return 0;
|
|
}
|
|
|
|
/* Get range, N-M. */
|
|
p = strchr(range, '-');
|
|
if (p == NULL) {
|
|
/* Exact match. */
|
|
if (pw_policy_parse_num(range, n) != 0)
|
|
return EINVAL;
|
|
|
|
*m = *n;
|
|
|
|
return 0;
|
|
}
|
|
*p++ = '\0';
|
|
|
|
if (pw_policy_parse_num(range, n) != 0 ||
|
|
pw_policy_parse_num(p, m) != 0)
|
|
return EINVAL;
|
|
|
|
return (0);
|
|
}
|
|
|
|
/*ARGSUSED*/
|
|
static int
|
|
pw_policy_handle_len(HANDLER_ARGS)
|
|
{
|
|
size_t len;
|
|
|
|
switch (flag) {
|
|
case LOAD_POLICY:
|
|
/* Here, '0' and '*' mean the same. */
|
|
if (pw_policy_parse_range(arg, &policy->minlen,
|
|
&policy->maxlen) != 0)
|
|
return EINVAL;
|
|
|
|
if (policy->minlen < 0)
|
|
policy->minlen = 0;
|
|
if (policy->maxlen < 0)
|
|
policy->maxlen = 0;
|
|
return 0;
|
|
|
|
case TEST_POLICY:
|
|
len = strlen(pw);
|
|
|
|
if ((policy->minlen && len < policy->minlen) ||
|
|
(policy->maxlen && len > policy->maxlen))
|
|
return EPERM;
|
|
return 0;
|
|
default:
|
|
|
|
return EINVAL;
|
|
}
|
|
}
|
|
|
|
static int
|
|
pw_policy_handle_charclass(HANDLER_ARGS)
|
|
{
|
|
int (*ischarclass)(int);
|
|
int32_t *n = NULL, *m = NULL, count = 0;
|
|
|
|
if (arg2 == islower) {
|
|
n = &policy->minlower;
|
|
m = &policy->maxlower;
|
|
} else if (arg2 == isupper) {
|
|
n = &policy->minupper;
|
|
m = &policy->maxupper;
|
|
} else if (arg2 == isdigit) {
|
|
n = &policy->mindigits;
|
|
m = &policy->maxdigits;
|
|
} else if (arg2 == ispunct) {
|
|
n = &policy->minpunct;
|
|
m = &policy->maxpunct;
|
|
} else
|
|
return EINVAL;
|
|
|
|
switch (flag) {
|
|
case LOAD_POLICY:
|
|
if ((pw_policy_parse_range(arg, n, m) != 0))
|
|
return EINVAL;
|
|
return 0;
|
|
|
|
case TEST_POLICY:
|
|
ischarclass = arg2;
|
|
|
|
do {
|
|
if (!isspace((unsigned char)*pw) &&
|
|
ischarclass((unsigned char)*pw++))
|
|
count++;
|
|
} while (*pw != '\0');
|
|
|
|
if (MINMAX_ERR(*n, *m, count))
|
|
return EPERM;
|
|
return 0;
|
|
|
|
default:
|
|
return EINVAL;
|
|
}
|
|
}
|
|
|
|
/*ARGSUSED*/
|
|
static int
|
|
pw_policy_handle_nclasses(HANDLER_ARGS)
|
|
{
|
|
switch (flag) {
|
|
case LOAD_POLICY:
|
|
if (pw_policy_parse_range(arg, &policy->minclasses,
|
|
&policy->maxclasses) != 0)
|
|
return EINVAL;
|
|
|
|
/*
|
|
* Set these to -1 just in case. This indicates we allow any
|
|
* number of characters from all classes.
|
|
*/
|
|
policy->minlower = -1;
|
|
policy->maxlower = -1;
|
|
policy->minupper = -1;
|
|
policy->maxupper = -1;
|
|
policy->mindigits = -1;
|
|
policy->maxdigits = -1;
|
|
policy->minpunct = -1;
|
|
policy->maxpunct = -1;
|
|
return 0;
|
|
|
|
case TEST_POLICY: {
|
|
int have_lower = 0, have_upper = 0, have_digit = 0,
|
|
have_punct = 0;
|
|
int32_t nsets = 0;
|
|
|
|
while (*pw != '\0') {
|
|
if (islower((unsigned char)*pw) && !have_lower) {
|
|
have_lower = 1;
|
|
nsets++;
|
|
} else if (isupper((unsigned char)*pw) && !have_upper) {
|
|
have_upper = 1;
|
|
nsets++;
|
|
} else if (isdigit((unsigned char)*pw) && !have_digit) {
|
|
have_digit = 1;
|
|
nsets++;
|
|
} else if (ispunct((unsigned char)*pw) && !have_punct) {
|
|
have_punct = 1;
|
|
nsets++;
|
|
}
|
|
pw++;
|
|
}
|
|
|
|
if (MINMAX_ERR(policy->minclasses, policy->maxclasses, nsets))
|
|
return EPERM;
|
|
return 0;
|
|
}
|
|
default:
|
|
return EINVAL;
|
|
}
|
|
}
|
|
|
|
/*ARGSUSED*/
|
|
static int
|
|
pw_policy_handle_ntoggles(HANDLER_ARGS)
|
|
{
|
|
switch (flag) {
|
|
case LOAD_POLICY:
|
|
if (pw_policy_parse_range(arg, &policy->mintoggles,
|
|
&policy->maxtoggles) != 0)
|
|
return EINVAL;
|
|
return 0;
|
|
|
|
case TEST_POLICY: {
|
|
int previous_set = 0, current_set = 0;
|
|
int32_t ntoggles = 0, current_ntoggles = 0;
|
|
|
|
#define CHAR_CLASS(c) (islower((unsigned char)(c)) ? 1 : \
|
|
isupper((unsigned char)(c)) ? 2 : \
|
|
isdigit((unsigned char)(c)) ? 3 : \
|
|
ispunct((unsigned char)(c)) ? 4 : \
|
|
0)
|
|
|
|
while (*pw != '\0') {
|
|
if (!isspace((unsigned char)*pw)) {
|
|
current_set = CHAR_CLASS(*pw);
|
|
if (!current_set) {
|
|
return EINVAL;
|
|
}
|
|
|
|
if (!previous_set ||
|
|
current_set == previous_set) {
|
|
current_ntoggles++;
|
|
} else {
|
|
if (current_ntoggles > ntoggles)
|
|
ntoggles = current_ntoggles;
|
|
|
|
current_ntoggles = 1;
|
|
}
|
|
|
|
previous_set = current_set;
|
|
}
|
|
|
|
pw++;
|
|
}
|
|
|
|
#undef CHAR_CLASS
|
|
|
|
if (MINMAX_ERR(policy->mintoggles, policy->maxtoggles,
|
|
ntoggles))
|
|
return EPERM;
|
|
|
|
return 0;
|
|
}
|
|
default:
|
|
return EINVAL;
|
|
}
|
|
}
|
|
|
|
pw_policy_t
|
|
pw_policy_load(void *key, int how)
|
|
{
|
|
pw_policy_t policy;
|
|
struct pw_policy_handler *hp;
|
|
char buf[BUFSIZ];
|
|
|
|
/* If there's no /etc/passwd.conf, don't touch the policy. */
|
|
if (access(_PATH_PASSWD_CONF, R_OK) == -1)
|
|
return NULL;
|
|
|
|
/* No key provided. Use default. */
|
|
if (key == NULL) {
|
|
key = __UNCONST(PW_POLICY_DEFAULTKEY);
|
|
goto load_policies;
|
|
}
|
|
|
|
(void)memset(buf, 0, sizeof(buf));
|
|
|
|
errno = 0;
|
|
switch (how) {
|
|
case PW_POLICY_BYSTRING:
|
|
/*
|
|
* Check for provided key. If non-existant, fallback to
|
|
* the default.
|
|
*/
|
|
pw_getconf(buf, sizeof(buf), key, "foo");
|
|
if (errno == ENOTDIR)
|
|
key = __UNCONST(PW_POLICY_DEFAULTKEY);
|
|
|
|
break;
|
|
|
|
case PW_POLICY_BYPASSWD: {
|
|
struct passwd *pentry = key;
|
|
|
|
/*
|
|
* Check for policy for given user. If can't find any,
|
|
* try a policy for the login class. If can't find any,
|
|
* fallback to the default.
|
|
*/
|
|
|
|
pw_getconf(buf, sizeof(buf), pentry->pw_name, "foo");
|
|
if (errno == ENOTDIR) {
|
|
memset(buf, 0, sizeof(buf));
|
|
pw_getconf(buf, sizeof(buf), pentry->pw_class, "foo");
|
|
if (errno == ENOTDIR)
|
|
key = __UNCONST(PW_POLICY_DEFAULTKEY);
|
|
else
|
|
key = pentry->pw_class;
|
|
} else
|
|
key = pentry->pw_name;
|
|
|
|
break;
|
|
}
|
|
|
|
case PW_POLICY_BYGROUP: {
|
|
struct group *gentry = key;
|
|
|
|
/*
|
|
* Check for policy for given group. If can't find any,
|
|
* fallback to the default.
|
|
*/
|
|
pw_getconf(buf, sizeof(buf), gentry->gr_name, "foo");
|
|
if (errno == ENOTDIR)
|
|
key = __UNCONST(PW_POLICY_DEFAULTKEY);
|
|
else
|
|
key = gentry->gr_name;
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
/*
|
|
* Fail the policy because we don't know how to parse the
|
|
* key we were passed.
|
|
*/
|
|
errno = EINVAL;
|
|
return NULL;
|
|
}
|
|
|
|
load_policies:
|
|
policy = calloc(sizeof(struct pw_policy), 1);
|
|
if (policy == NULL)
|
|
return NULL;
|
|
|
|
hp = &handlers[0];
|
|
while (hp->name != NULL) {
|
|
int error;
|
|
|
|
(void)memset(buf, 0, sizeof(buf));
|
|
pw_getconf(buf, sizeof(buf), key, hp->name);
|
|
if (*buf) {
|
|
error = (*hp->handler)(policy, LOAD_POLICY, NULL, buf,
|
|
hp->arg2);
|
|
if (error) {
|
|
errno = error;
|
|
return NULL;
|
|
}
|
|
}
|
|
hp++;
|
|
}
|
|
|
|
return policy;
|
|
}
|
|
|
|
int
|
|
pw_policy_test(pw_policy_t policy, char *pw)
|
|
{
|
|
struct pw_policy_handler *hp;
|
|
|
|
if (policy == NULL) {
|
|
errno = EINVAL;
|
|
return 0;
|
|
}
|
|
|
|
hp = &handlers[0];
|
|
while (hp->name != NULL) {
|
|
int error;
|
|
error = (*hp->handler)(policy, TEST_POLICY, pw, NULL, hp->arg2);
|
|
if (error) {
|
|
errno = error;
|
|
return -1;
|
|
}
|
|
hp++;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
pw_policy_free(pw_policy_t policy)
|
|
{
|
|
_DIAGASSERT(policy != NULL);
|
|
|
|
free(policy);
|
|
}
|