Start 1.33.0 development cycle
[oweals/busybox.git] / libbb / obscure.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * Mini weak password checker implementation for busybox
4  *
5  * Copyright (C) 2006 Tito Ragusa <farmatito@tiscali.it>
6  *
7  * Licensed under GPLv2 or later, see file LICENSE in this source tree.
8  */
9
10 /*      A good password:
11         1)      should contain at least six characters (man passwd);
12         2)      empty passwords are not permitted;
13         3)      should contain a mix of four different types of characters
14                 upper case letters,
15                 lower case letters,
16                 numbers,
17                 special characters such as !@#$%^&*,;".
18         This password types should not  be permitted:
19         a)      pure numbers: birthdates, social security number, license plate, phone numbers;
20         b)      words and all letters only passwords (uppercase, lowercase or mixed)
21                 as palindromes, consecutive or repetitive letters
22                 or adjacent letters on your keyboard;
23         c)      username, real name, company name or (e-mail?) address
24                 in any form (as-is, reversed, capitalized, doubled, etc.).
25                 (we can check only against username, gecos and hostname)
26         d)      common and obvious letter-number replacements
27                 (e.g. replace the letter O with number 0)
28                 such as "M1cr0$0ft" or "P@ssw0rd" (CAVEAT: we cannot check for them
29                 without the use of a dictionary).
30
31         For each missing type of characters an increase of password length is
32         requested.
33
34         If user is root we warn only.
35
36         CAVEAT: some older versions of crypt() truncates passwords to 8 chars,
37         so that aaaaaaaa1Q$ is equal to aaaaaaaa making it possible to fool
38         some of our checks. We don't test for this special case as newer versions
39         of crypt do not truncate passwords.
40 */
41
42 #include "libbb.h"
43
44 static int string_checker_helper(const char *p1, const char *p2) __attribute__ ((__pure__));
45
46 static int string_checker_helper(const char *p1, const char *p2)
47 {
48         /* as sub-string */
49         if (strcasestr(p2, p1) != NULL
50         /* invert in case haystack is shorter than needle */
51          || strcasestr(p1, p2) != NULL
52         /* as-is or capitalized */
53         /* || strcasecmp(p1, p2) == 0 - 1st strcasestr should catch this too */
54         ) {
55                 return 1;
56         }
57         return 0;
58 }
59
60 static int string_checker(const char *p1, const char *p2)
61 {
62         int size, i;
63         /* check string */
64         int ret = string_checker_helper(p1, p2);
65         /* make our own copy */
66         char *p = xstrdup(p1);
67
68         /* reverse string */
69         i = size = strlen(p1);
70         while (--i >= 0) {
71                 *p++ = p1[i];
72         }
73         p -= size; /* restore pointer */
74
75         /* check reversed string */
76         ret |= string_checker_helper(p, p2);
77
78         /* clean up */
79         nuke_str(p);
80         free(p);
81
82         return ret;
83 }
84
85 #define CATEGORIES  4
86
87 #define LOWERCASE   1
88 #define UPPERCASE   2
89 #define NUMBERS     4
90 #define SPECIAL     8
91
92 #define LAST_CAT    8
93
94 static const char *obscure_msg(const char *old_p, const char *new_p, const struct passwd *pw)
95 {
96         unsigned length;
97         unsigned size;
98         unsigned mixed;
99         unsigned c;
100         unsigned i;
101         const char *p;
102         char *hostname;
103
104         /* size */
105         if (!new_p || (length = strlen(new_p)) < CONFIG_PASSWORD_MINLEN)
106                 return "too short";
107
108         /* no username as-is, as sub-string, reversed, capitalized, doubled */
109         if (string_checker(new_p, pw->pw_name)) {
110                 return "similar to username";
111         }
112 #ifndef __BIONIC__
113         /* no gecos as-is, as sub-string, reversed, capitalized, doubled */
114         if (pw->pw_gecos[0] && string_checker(new_p, pw->pw_gecos)) {
115                 return "similar to gecos";
116         }
117 #endif
118         /* hostname as-is, as sub-string, reversed, capitalized, doubled */
119         hostname = safe_gethostname();
120         i = string_checker(new_p, hostname);
121         free(hostname);
122         if (i)
123                 return "similar to hostname";
124
125         /* Should / Must contain a mix of: */
126         mixed = 0;
127         for (i = 0; i < length; i++) {
128                 if (islower(new_p[i])) {        /* a-z */
129                         mixed |= LOWERCASE;
130                 } else if (isupper(new_p[i])) { /* A-Z */
131                         mixed |= UPPERCASE;
132                 } else if (isdigit(new_p[i])) { /* 0-9 */
133                         mixed |= NUMBERS;
134                 } else  {                       /* special characters */
135                         mixed |= SPECIAL;
136                 }
137                 /* Count i'th char */
138                 c = 0;
139                 p = new_p;
140                 while (1) {
141                         p = strchr(p, new_p[i]);
142                         if (p == NULL) {
143                                 break;
144                         }
145                         c++;
146                         p++;
147                         if (!*p) {
148                                 break;
149                         }
150                 }
151                 /* More than 50% similar characters ? */
152                 if (c*2 >= length) {
153                         return "too many similar characters";
154                 }
155         }
156
157         size = CONFIG_PASSWORD_MINLEN + 2*CATEGORIES;
158         for (i = 1; i <= LAST_CAT; i <<= 1)
159                 if (mixed & i)
160                         size -= 2;
161         if (length < size)
162                 return "too weak";
163
164         if (old_p && old_p[0]) {
165                 /* check vs. old password */
166                 if (string_checker(new_p, old_p)) {
167                         return "similar to old password";
168                 }
169         }
170
171         return NULL;
172 }
173
174 int FAST_FUNC obscure(const char *old, const char *newval, const struct passwd *pw)
175 {
176         const char *msg;
177
178         msg = obscure_msg(old, newval, pw);
179         if (msg) {
180                 printf("Bad password: %s\n", msg);
181                 return 1;
182         }
183         return 0;
184 }
185
186 #if ENABLE_UNIT_TEST
187
188 /* Test obscure_msg() instead of obscure() in order not to print anything. */
189
190 static const struct passwd pw = {
191         .pw_name = (char *)"johndoe",
192         .pw_gecos = (char *)"John Doe",
193 };
194
195 BBUNIT_DEFINE_TEST(obscure_weak_pass)
196 {
197         /* Empty password */
198         BBUNIT_ASSERT_NOTNULL(obscure_msg("Ad4#21?'S|", "", &pw));
199         /* Pure numbers */
200         BBUNIT_ASSERT_NOTNULL(obscure_msg("Ad4#21?'S|", "23577315", &pw));
201         /* Similar to pw_name */
202         BBUNIT_ASSERT_NOTNULL(obscure_msg("Ad4#21?'S|", "johndoe123%", &pw));
203         /* Similar to pw_gecos, reversed */
204         BBUNIT_ASSERT_NOTNULL(obscure_msg("Ad4#21?'S|", "eoD nhoJ^44@", &pw));
205         /* Similar to the old password */
206         BBUNIT_ASSERT_NOTNULL(obscure_msg("Ad4#21?'S|", "d4#21?'S", &pw));
207         /* adjacent letters */
208         BBUNIT_ASSERT_NOTNULL(obscure_msg("Ad4#21?'S|", "qwerty123", &pw));
209         /* Many similar chars */
210         BBUNIT_ASSERT_NOTNULL(obscure_msg("Ad4#21?'S|", "^33Daaaaaa1", &pw));
211
212         BBUNIT_ENDTEST;
213 }
214
215 BBUNIT_DEFINE_TEST(obscure_strong_pass)
216 {
217         BBUNIT_ASSERT_NULL(obscure_msg("Rt4##2&:'|", "}(^#rrSX3S*22", &pw));
218
219         BBUNIT_ENDTEST;
220 }
221
222 #endif /* ENABLE_UNIT_TEST */