File: pwpolicy.c

package info (click to toggle)
gvm-libs 22.35.4-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 2,980 kB
  • sloc: ansic: 39,095; makefile: 26
file content (410 lines) | stat: -rw-r--r-- 12,121 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
/* SPDX-FileCopyrightText: 2013-2023 Greenbone AG
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

/**
 * @file
 * @brief Check passwords against a list of pattern
 *
 * See \ref PWPOLICY_FILE_NAME for a syntax description of the pattern
 * file.
 */

#include "pwpolicy.h"

#include <errno.h> /* for errno */
#include <glib.h>  /* for g_strdup_printf, g_ascii_strcasecmp, g_free, ... */
#include <stdio.h> /* for fclose, fgets, fopen, FILE, ferror, EOF, getc */
#include <stdlib.h>
#include <string.h> /* for strstr, strlen, strncmp */

#ifndef DIM
#define DIM(v) (sizeof (v) / sizeof ((v)[0]))
#define DIMof(type, member) DIM (((type *) 0)->member)
#endif

#undef G_LOG_DOMAIN
/**
 * @brief GLib log domain.
 */
#define G_LOG_DOMAIN "libgvm base"

/**
 * @brief The name of the pattern file
 *
 * This file contains pattern with bad passphrases.  The file is line
 * based with maximum length of 255 bytes per line and expected to be
 * in UTF-8 encoding.  Each line may either be a comment line, a
 * simple string, a regular expression or a processing instruction.
 * The lines are parsed sequentially.
 *
 * *Comments* are indicated by a hash mark ('#') as the first non
 * white-space character of a line followed immediately by a space or
 * end of line.  Such a comment line is completely ignored.
 *
 * *Simple strings* start after optional leading white-space.  They
 * are compared to the password under validation.  The comparison is
 * case insensitive for all ASCII characters.
 *
 * *Regular expressions* start after optional leading white-space with
 * either a single slash ('/') or an exclamation mark ('!') directly
 * followed by a slash.  They extend to the end of the line but may be
 * terminated with another slash which may then only be followed by
 * more white-space.  The regular expression are Perl Compatible
 * Regular Expressions (PCRE) and are by default case insensitive.  If
 * the regular expression line starts with the exclamation mark, the
 * match is reversed; i.e. an error is returned if the password does
 * not match.
 *
 * *Processing instructions* are special comments to control the
 * operation of the policy checking.  The start like a comment but the
 * hash mark is immediately followed by a plus ('+') signed, a
 * keyword, an optional colon (':') and an optional value string.  The
 * following processing instructions are supported:
 *
 *   #+desc[:] STRING
 *
 *     This is used to return a meaningful error message.  STRING is
 *     used a the description for all errors up to the next /desc/ or
 *     /nodesc/ processing instruction.
 *
 *   #+nodesc
 *
 *     This is syntactic sugar for /desc/ without a value.  It
 *     switches back to a default error description (pattern file name
 *     and line number).
 *
 *   #+search[:] FILENAME
 *
 *     This searches the file with name FILENAME for a match.  The
 *     comparison is case insensitive for all ASCII characters.  This
 *     is a simple linear search and stops at the first match.
 *     Comments are not allowed in that file.  A line in that file may
 *     not be longer than 255 characters.  An example for such a file
 *     is "/usr/share/dict/words".
 *
 *   #+username
 *
 *     This is used to perform checks on the name/password
 *     combination.  Currently this checks whether the password
 *     matches or is included in the password. It may eventually be
 *     extended to further tests.
 */
#define PWPOLICY_FILE_NAME GVM_SYSCONF_DIR "/pwpolicy.conf"

/**
 * @brief Flag indicating that passwords are not checked.
 */
static gboolean disable_password_policy;

/**
 * @return A malloced string to be returned on read and configuration
 * errors.
 */
static char *
policy_checking_failed (void)
{
  return g_strdup ("Password policy checking failed (internal error)");
}

/**
 * @brief Check whether a string starts with a keyword
 *
 * Note that the keyword may optionally be terminated by a colon.
 *
 * @param string   The string to check
 * @param keyword  The keyword
 *
 * @return NULL if the keyword is not found.  If found a pointer into
 *         \p string to the value of the keyword with removed leading
 *         spaces is returned.
 */
static char *
is_keyword (char *string, const char *keyword)
{
  int idx, slen;
  char *tmp;
  idx = strlen (keyword);
  slen = strlen (string);

  if (!strncmp (string, keyword, idx))
    {
      tmp = string + idx;
      if (tmp - string > slen)
        return NULL;
      // skip optional:
      if (*tmp == ':')
        tmp++;
      if (tmp - string > slen)
        return NULL;

      for (; tmp - string < slen && g_ascii_isspace (*tmp); tmp++)
        {
          // skip whitespace
        }
      return tmp;
    }
  return NULL;
}

/**
 * @brief Search a file for a matching line
 *
 * This is a case insensitive search for a password in a file.  The
 * file is assumed to be a simple LF delimited list of words.
 *
 * @param fname    Name of the file to search.
 * @param password Password to search for.
 *
 * @return -1 if the file could not be opened or a read error
 *         occurred, 0 if password was not found and 1 if password was found.
 */
static int
search_file (const char *fname, const char *password)
{
  FILE *fp;
  int c;
  char line[256];

  fp = fopen (fname, "r");
  if (!fp)
    return -1;

  while (fgets (line, DIM (line) - 1, fp))
    {
      size_t len;

      len = strlen (line);
      if (!len || line[len - 1] != '\n')
        {
          /* Incomplete last line or line too long.  Eat until end of
             line. */
          while ((c = getc (fp)) != EOF && c != '\n')
            ;
          continue;
        }
      line[--len] = 0; /* Chop the LF. */
      if (len && line[len - 1] == '\r')
        line[--len] = 0; /* Chop an optional CR. */
      if (!len)
        continue; /* Empty */
      if (!g_ascii_strcasecmp (line, password))
        {
          fclose (fp);
          return 1; /* Found.  */
        }
    }
  if (ferror (fp))
    {
      int save_errno = errno;
      fclose (fp);
      errno = save_errno;
      return -1; /* Read error.  */
    }
  fclose (fp);
  return 0; /* Not found.  */
}

/**
 * @brief Parse one line of a pettern file
 *
 * @param line     A null terminated buffer with the content of the line.
 *                 The line terminator has already been stripped. It may
 *                 be modified after return.
 * @param fname    The name of the pattern file for error reporting
 * @param lineno   The current line number for error reporting
 * @param descp    Pointer to a variable holding the current description
 *                 string or NULL for no description.
 * @param password The password to check.
 * @param username The username to check.
 *
 * @return NULL on success or a malloced string with an error
 *         description.
 */
static char *
parse_pattern_line (char *line, const char *fname, int lineno, char **descp,
                    const char *password, const char *username)
{
  char *ret = NULL;
  char *p;
  size_t n;

  /* Skip leading spaces.  */
  while (g_ascii_isspace (*line))
    line++;

  if (!*line) /* Empty line.  */
    {
      ret = NULL;
    }
  else if (*line == '#' && line[1] == '+') /* Processing instruction.  */
    {
      line += 2;
      p = is_keyword (line, "desc");
      if (p)
        {
          g_free (*descp);
          if (*p)
            *descp = g_strdup (p);
          else
            *descp = NULL;
        }
      else if ((is_keyword (line, "nodesc")))
        {
          g_free (*descp);
          *descp = NULL;
        }
      else if ((p = is_keyword (line, "search")))
        {
          int sret;

          sret = search_file (p, password);
          if (sret == -1)
            {
              g_warning ("error searching '%s' (requested at line %d): %s", p,
                         lineno, g_strerror (errno));
              ret = policy_checking_failed ();
            }
          else if (sret && *descp)
            ret = g_strdup_printf ("Weak password (%s)", *descp);
          else if (sret)
            ret = g_strdup_printf ("Weak password (found in '%s')", p);
          else
            ret = NULL;
        }
      else if (is_keyword (line, "username"))
        {
          /* Fixme: The include check is case sensitive and the strcmp
             does only work with ascii.  Changing this required a bit
             more more (g_utf8_casefold) and also requires checking
             for valid utf8 sequences in the password and all pattern.  */
          if (!username)
            ret = NULL;
          else if (!g_ascii_strcasecmp (password, username))
            ret = g_strdup_printf ("Weak password (%s)",
                                   "user name matches password");
          else if (strstr (password, username))
            ret = g_strdup_printf ("Weak password (%s)",
                                   "user name is part of the password");
          else if (strstr (username, password))
            ret = g_strdup_printf ("Weak password (%s)",
                                   "password is part of the user name");
          else
            ret = NULL;
        }
      else
        {
          g_warning ("error reading '%s', line %d: %s", fname, lineno,
                     "unknown processing instruction");
          ret = policy_checking_failed ();
        }
    }
  else if (*line == '#') /* Comment */
    {
      ret = NULL;
    }
  else if (*line == '/'
           || (*line == '!' && line[1] == '/')) /* Regular expression.  */
    {
      int rev = (*line == '!');
      if (rev)
        line++;
      line++;
      n = strlen (line);
      if (n && line[n - 1] == '/')
        line[n - 1] = 0;
      if (((!g_regex_match_simple (line, password, G_REGEX_CASELESS, 0)) ^ rev))
        ret = NULL;
      else if (*descp)
        ret = g_strdup_printf ("Weak password (%s)", *descp);
      else
        ret =
          g_strdup_printf ("Weak password (see '%s' line %d)", fname, lineno);
    }
  else /* Simple string.  */
    {
      if (g_ascii_strcasecmp (line, password))
        ret = NULL;
      else if (*descp)
        ret = g_strdup_printf ("Weak password (%s)", *descp);
      else
        ret =
          g_strdup_printf ("Weak password (see '%s' line %d)", fname, lineno);
    }

  return ret;
}

/**
 * @brief Validate a password against the pattern file
 *
 * @param[in] password  The password to check
 * @param[in] username  The user name or NULL.  This is used to check
 *                      the passphrase against the user name.
 *
 * @return NULL on success or a malloced string with an error
 *         description.
 */
char *
gvm_validate_password (const char *password, const char *username)
{
  const char *patternfile = PWPOLICY_FILE_NAME;
  char *ret;
  FILE *fp;
  int lineno;
  char line[256];
  char *desc = NULL;

  if (disable_password_policy)
    return NULL;

  if (!password || !*password)
    return g_strdup ("Empty password");

  fp = fopen (patternfile, "r");
  if (!fp)
    {
      g_warning ("error opening '%s': %s", patternfile, g_strerror (errno));
      return policy_checking_failed ();
    }
  lineno = 0;
  ret = NULL;
  while (fgets (line, DIM (line) - 1, fp))
    {
      size_t len;

      lineno++;
      len = strlen (line);
      if (!len || line[len - 1] != '\n')
        {
          g_warning ("error reading '%s', line %d: %s", patternfile, lineno,
                     len ? "line too long" : "line without a LF");
          ret = policy_checking_failed ();
          break;
        }
      line[--len] = 0; /* Chop the LF. */
      if (len && line[len - 1] == '\r')
        line[--len] = 0; /* Chop an optional CR. */
      ret = parse_pattern_line (line, patternfile, lineno, &desc, password,
                                username);
      if (ret)
        break;

      bzero (line, sizeof (line));
    }

  fclose (fp);
  g_free (desc);
  return ret;
}

/**
 * @brief Disable all password policy checking
 */
void
gvm_disable_password_policy (void)
{
  disable_password_policy = TRUE;
  g_warning ("Password policy checking has been disabled.");
}