mirror of
git://git.gnupg.org/gnupg.git
synced 2024-12-22 10:19:57 +01:00
tools: Extend gpg-check-pattern.
* tools/gpg-check-pattern.c: Major rewrite. -- Signed-off-by: Werner Koch <wk@gnupg.org> Here is a simple pattern file: ==================== # Pattern to reject passwords which do not comply to # - at least 1 uppercase letter # - at least 1 lowercase letter # - at least one number # - at least one special character # and a few extra things to show the reject mode # Reject is the default mode, ignore case is the default #[reject] #[icase] # If the password starts with "foo" (case insensitive) it is rejected. /foo.*/ [case] # If the password starts with "bar" (case sensitive) it is rejected. /bar.*/ # Switch to accept mode: Only if all patterns up to the next "accept" # or "reject" tag or EOF match, the password is accepted. Otherwise # the password is rejected. [accept] /[A-Z]+/ /[a-z]+/ /[0-9]+/ /[^A-Za-z0-9]+/ ================= Someone™ please write regression tests.
This commit is contained in:
parent
5c8124b8b9
commit
73c03e0232
@ -2081,6 +2081,51 @@ gpgtar --list-archive test1
|
||||
@command{gpg-check-pattern} checks a passphrase given on stdin against
|
||||
a specified pattern file.
|
||||
|
||||
The pattern file is line based with comment lines beginning on the
|
||||
@emph{first} position with a @code{#}. Empty lines and lines with
|
||||
only white spaces are ignored. The actual pattern lines may either be
|
||||
verbatim string pattern and match as they are (trailing spaces are
|
||||
ignored) or extended regular expressions indicated by a @code{/} or
|
||||
@code{!/} in the first column and terminated by another @code{/} or
|
||||
end of line. If a regular expression starts with @code{!/} the match
|
||||
result is reversed. By default all comparisons are case insensitive.
|
||||
|
||||
Tag lines may be used to further control the operation of this tool.
|
||||
The currently defined tags are:
|
||||
|
||||
@table @code
|
||||
@item [icase]
|
||||
Switch to case insensitive comparison for all further patterns. This
|
||||
is the default.
|
||||
|
||||
@item [case]
|
||||
Switch to case sensitive comparison for all further patterns.
|
||||
|
||||
@item [reject]
|
||||
Switch to reject mode. This is the default mode.
|
||||
|
||||
@item [accept]
|
||||
Switch to accept mode.
|
||||
@end table
|
||||
|
||||
In the future more tags may be introduced and thus it is advisable not to
|
||||
start a plain pattern string with an open bracket. The tags must be
|
||||
given verbatim on the line with no spaces to the left or any non white
|
||||
space characters to the right.
|
||||
|
||||
In reject mode the program exits on the first match with an exit code
|
||||
of 1 (failure). If at the end of the pattern list the reject mode is
|
||||
still active the program exits with code 0 (success).
|
||||
|
||||
In accept mode blocks of patterns are used. A block starts at the
|
||||
next pattern after an "accept" tag and ends with the last pattern
|
||||
before the next "accept" or "reject" tag or at the end of the pattern
|
||||
list. If all patterns in a block match the program exits with an exit
|
||||
code of 0 (success). If any pattern in a block do not match the next
|
||||
pattern block is evaluated. If at the end of the pattern list the
|
||||
accept mode is still active the program exits with code 1 (failure).
|
||||
|
||||
|
||||
@mansect options
|
||||
@noindent
|
||||
|
||||
@ -2102,6 +2147,6 @@ Input is expected to be null delimited.
|
||||
|
||||
@mansect see also
|
||||
@ifset isman
|
||||
@command{gpg}(1),
|
||||
@command{gpg-agent}(1),
|
||||
@end ifset
|
||||
@include see-also-note.texi
|
||||
|
@ -1,4 +1,5 @@
|
||||
/* gpg-check-pattern.c - A tool to check passphrases against pattern.
|
||||
* Copyright (C) 2021 g10 Code GmbH
|
||||
* Copyright (C) 2007 Free Software Foundation, Inc.
|
||||
*
|
||||
* This file is part of GnuPG.
|
||||
@ -26,7 +27,6 @@
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <assert.h>
|
||||
#ifdef HAVE_LOCALE_H
|
||||
# include <locale.h>
|
||||
#endif
|
||||
@ -50,11 +50,7 @@
|
||||
enum cmd_and_opt_values
|
||||
{ aNull = 0,
|
||||
oVerbose = 'v',
|
||||
oArmor = 'a',
|
||||
oPassphrase = 'P',
|
||||
|
||||
oProtect = 'p',
|
||||
oUnprotect = 'u',
|
||||
oNull = '0',
|
||||
|
||||
oNoVerbose = 500,
|
||||
@ -101,6 +97,10 @@ struct pattern_s
|
||||
{
|
||||
int type;
|
||||
unsigned int lineno; /* Line number of the pattern file. */
|
||||
unsigned int newblock; /* First pattern in a new block. */
|
||||
unsigned int icase:1; /* Case insensitive match. */
|
||||
unsigned int accept:1; /* In accept mode. */
|
||||
unsigned int reverse:1; /* Reverse the outcome of a regexp match. */
|
||||
union {
|
||||
struct {
|
||||
const char *string; /* Pointer to the actual string (nul termnated). */
|
||||
@ -200,7 +200,7 @@ main (int argc, char **argv )
|
||||
gpgrt_usage (1);
|
||||
|
||||
/* We read the entire pattern file into our memory and parse it
|
||||
using a separate function. This allows us to eventual do the
|
||||
using a separate function. This allows us to eventually do the
|
||||
reading while running setuid so that the pattern file can be
|
||||
hidden from regular users. I am not sure whether this makes
|
||||
sense, but lets be prepared for it. */
|
||||
@ -219,7 +219,7 @@ main (int argc, char **argv )
|
||||
#endif
|
||||
process (stdin, patternarray);
|
||||
|
||||
return log_get_errorcount(0)? 1 : 0;
|
||||
return 4; /*NOTREACHED*/
|
||||
}
|
||||
|
||||
|
||||
@ -310,6 +310,7 @@ get_regerror (int errcode, regex_t *compiled)
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
/* Parse the pattern given in the memory aread DATA/DATALEN and return
|
||||
a new pattern array. The end of the array is indicated by a NULL
|
||||
entry. On error an error message is printed and the function
|
||||
@ -324,6 +325,9 @@ parse_pattern_file (char *data, size_t datalen)
|
||||
pattern_t *array;
|
||||
size_t arraysize, arrayidx;
|
||||
unsigned int lineno = 0;
|
||||
unsigned int icase_mode = 1;
|
||||
unsigned int accept_mode = 0;
|
||||
unsigned int newblock = 1; /* The first implict block. */
|
||||
|
||||
/* Estimate the number of entries by counting the non-comment lines. */
|
||||
arraysize = 0;
|
||||
@ -349,7 +353,7 @@ parse_pattern_file (char *data, size_t datalen)
|
||||
}
|
||||
else
|
||||
p2 = p + datalen;
|
||||
assert (!*p2);
|
||||
log_assert (!*p2);
|
||||
p2--;
|
||||
while (isascii (*p) && isspace (*p))
|
||||
p++;
|
||||
@ -359,23 +363,57 @@ parse_pattern_file (char *data, size_t datalen)
|
||||
*p2-- = 0;
|
||||
if (!*p)
|
||||
continue;
|
||||
assert (arrayidx < arraysize);
|
||||
if (!strcmp (p, "[case]"))
|
||||
{
|
||||
icase_mode = 0;
|
||||
continue;
|
||||
}
|
||||
if (!strcmp (p, "[icase]"))
|
||||
{
|
||||
icase_mode = 1;
|
||||
continue;
|
||||
}
|
||||
if (!strcmp (p, "[accept]"))
|
||||
{
|
||||
accept_mode = 1;
|
||||
newblock = 1;
|
||||
continue;
|
||||
}
|
||||
if (!strcmp (p, "[reject]"))
|
||||
{
|
||||
accept_mode = 0;
|
||||
newblock = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
log_assert (arrayidx < arraysize);
|
||||
array[arrayidx].lineno = lineno;
|
||||
if (*p == '/')
|
||||
array[arrayidx].icase = icase_mode;
|
||||
array[arrayidx].accept = accept_mode;
|
||||
array[arrayidx].reverse = 0;
|
||||
array[arrayidx].newblock = newblock;
|
||||
newblock = 0;
|
||||
|
||||
if (*p == '/' || (*p == '!' && p[1] == '/'))
|
||||
{
|
||||
int rerr;
|
||||
int reverse;
|
||||
|
||||
reverse = (*p == '!');
|
||||
p++;
|
||||
if (reverse)
|
||||
p++;
|
||||
array[arrayidx].type = PAT_REGEX;
|
||||
if (*p && p[strlen(p)-1] == '/')
|
||||
p[strlen(p)-1] = 0; /* Remove optional delimiter. */
|
||||
array[arrayidx].u.r.regex = xcalloc (1, sizeof (regex_t));
|
||||
array[arrayidx].reverse = reverse;
|
||||
rerr = regcomp (array[arrayidx].u.r.regex, p,
|
||||
REG_ICASE|REG_EXTENDED);
|
||||
(array[arrayidx].icase? REG_ICASE:0)|REG_EXTENDED);
|
||||
if (rerr)
|
||||
{
|
||||
char *rerrbuf = get_regerror (rerr, array[arrayidx].u.r.regex);
|
||||
log_error ("invalid r.e. at line %u: %s\n", lineno, rerrbuf);
|
||||
log_error ("invalid regexp at line %u: %s\n", lineno, rerrbuf);
|
||||
xfree (rerrbuf);
|
||||
if (!opt.checkonly)
|
||||
exit (1);
|
||||
@ -383,25 +421,44 @@ parse_pattern_file (char *data, size_t datalen)
|
||||
}
|
||||
else
|
||||
{
|
||||
if (*p == '[')
|
||||
{
|
||||
static int shown;
|
||||
|
||||
if (!shown)
|
||||
{
|
||||
log_info ("future warning: do no start a string with '['"
|
||||
" but use a regexp (line %u)\n", lineno);
|
||||
shown = 1;
|
||||
}
|
||||
}
|
||||
array[arrayidx].type = PAT_STRING;
|
||||
array[arrayidx].u.s.string = p;
|
||||
array[arrayidx].u.s.length = strlen (p);
|
||||
}
|
||||
|
||||
arrayidx++;
|
||||
}
|
||||
assert (arrayidx < arraysize);
|
||||
log_assert (arrayidx < arraysize);
|
||||
array[arrayidx].type = PAT_NULL;
|
||||
|
||||
if (lineno && newblock)
|
||||
log_info ("warning: pattern list ends with a singleton"
|
||||
" accept or reject tag\n");
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
|
||||
/* Check whether string macthes any of the pattern in PATARRAY and
|
||||
/* Check whether string matches any of the pattern in PATARRAY and
|
||||
returns the matching pattern item or NULL. */
|
||||
static pattern_t *
|
||||
match_p (const char *string, pattern_t *patarray)
|
||||
{
|
||||
pattern_t *pat;
|
||||
int match;
|
||||
int accept_match; /* Tracks matchinf state in an accept block. */
|
||||
int accept_skip; /* Skip remaining patterns in an accept block. */
|
||||
|
||||
if (!*string)
|
||||
{
|
||||
@ -410,30 +467,84 @@ match_p (const char *string, pattern_t *patarray)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
accept_match = 0;
|
||||
accept_skip = 0;
|
||||
for (pat = patarray; pat->type != PAT_NULL; pat++)
|
||||
{
|
||||
match = 0;
|
||||
if (pat->newblock)
|
||||
accept_match = accept_skip = 0;
|
||||
|
||||
if (pat->type == PAT_STRING)
|
||||
{
|
||||
if (!strcasecmp (pat->u.s.string, string))
|
||||
return pat;
|
||||
if (pat->icase)
|
||||
{
|
||||
if (!strcasecmp (pat->u.s.string, string))
|
||||
match = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!strcmp (pat->u.s.string, string))
|
||||
match = 1;
|
||||
}
|
||||
}
|
||||
else if (pat->type == PAT_REGEX)
|
||||
{
|
||||
int rerr;
|
||||
|
||||
rerr = regexec (pat->u.r.regex, string, 0, NULL, 0);
|
||||
if (pat->reverse)
|
||||
{
|
||||
if (!rerr)
|
||||
rerr = REG_NOMATCH;
|
||||
else if (rerr == REG_NOMATCH)
|
||||
rerr = 0;
|
||||
}
|
||||
|
||||
if (!rerr)
|
||||
return pat;
|
||||
match = 1;
|
||||
else if (rerr != REG_NOMATCH)
|
||||
{
|
||||
char *rerrbuf = get_regerror (rerr, pat->u.r.regex);
|
||||
log_error ("matching r.e. failed: %s\n", rerrbuf);
|
||||
log_error ("matching regexp failed: %s\n", rerrbuf);
|
||||
xfree (rerrbuf);
|
||||
return pat; /* Better indicate a match on error. */
|
||||
if (pat->accept)
|
||||
match = 0; /* Better indicate no match on error. */
|
||||
else
|
||||
match = 1; /* Better indicate a match on error. */
|
||||
}
|
||||
}
|
||||
else
|
||||
BUG ();
|
||||
|
||||
if (pat->accept)
|
||||
{
|
||||
/* Accept mode: all patterns in the accept block must match.
|
||||
* Thus we need to check whether the next pattern has a
|
||||
* transition and act only then. */
|
||||
if (match && !accept_skip)
|
||||
accept_match = 1;
|
||||
else
|
||||
{
|
||||
accept_match = 0;
|
||||
accept_skip = 1;
|
||||
}
|
||||
|
||||
if (pat[1].type == PAT_NULL || pat[1].newblock)
|
||||
{
|
||||
/* Transition detected. Note that this also handles the
|
||||
* end of pattern loop case. */
|
||||
if (accept_match)
|
||||
return pat;
|
||||
/* The next is not really but we do it for clarity. */
|
||||
accept_match = accept_skip = 0;
|
||||
}
|
||||
}
|
||||
else /* Reject mode: Return true on the first match. */
|
||||
{
|
||||
if (match)
|
||||
return pat;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@ -449,6 +560,7 @@ process (FILE *fp, pattern_t *patarray)
|
||||
int c;
|
||||
unsigned long lineno = 0;
|
||||
pattern_t *pat;
|
||||
int last_is_accept;
|
||||
|
||||
idx = 0;
|
||||
c = 0;
|
||||
@ -468,17 +580,28 @@ process (FILE *fp, pattern_t *patarray)
|
||||
pat = match_p (buffer, patarray);
|
||||
if (pat)
|
||||
{
|
||||
/* Note that the accept mode works correctly only with
|
||||
* one input line. */
|
||||
if (opt.verbose)
|
||||
log_error ("input line %lu matches pattern at line %u"
|
||||
" - rejected\n",
|
||||
lineno, pat->lineno);
|
||||
exit (1);
|
||||
log_info ("input line %lu matches pattern at line %u"
|
||||
" - %s\n",
|
||||
lineno, pat->lineno,
|
||||
pat->accept? "accepted":"rejected");
|
||||
}
|
||||
idx = 0;
|
||||
wipememory (buffer, sizeof buffer);
|
||||
if (pat)
|
||||
{
|
||||
if (pat->accept)
|
||||
exit (0);
|
||||
else
|
||||
exit (1);
|
||||
}
|
||||
}
|
||||
else
|
||||
idx++;
|
||||
}
|
||||
wipememory (buffer, sizeof buffer);
|
||||
if (c != EOF)
|
||||
{
|
||||
log_error ("input line %lu too long - rejected\n", lineno+1);
|
||||
@ -490,6 +613,20 @@ process (FILE *fp, pattern_t *patarray)
|
||||
lineno+1, strerror (errno));
|
||||
exit (1);
|
||||
}
|
||||
|
||||
/* Check last pattern to see whether we are in accept mode. */
|
||||
last_is_accept = 0;
|
||||
for (pat = patarray; pat->type != PAT_NULL; pat++)
|
||||
last_is_accept = pat->accept;
|
||||
|
||||
if (opt.verbose)
|
||||
log_info ("no input line matches the pattern - accepted\n");
|
||||
log_info ("no input line matches the pattern - %s\n",
|
||||
last_is_accept? "rejected":"accepted");
|
||||
|
||||
if (log_get_errorcount(0))
|
||||
exit (2); /* Ooops - reject. */
|
||||
else if (last_is_accept)
|
||||
exit (1); /* Reject */
|
||||
else
|
||||
exit (0); /* Accept */
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user