line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Data::Transpose::PasswordPolicy; |
2
|
|
|
|
|
|
|
|
3
|
5
|
|
|
5
|
|
29028
|
use 5.010001; |
|
5
|
|
|
|
|
18
|
|
4
|
5
|
|
|
5
|
|
69
|
use strict; |
|
5
|
|
|
|
|
11
|
|
|
5
|
|
|
|
|
157
|
|
5
|
5
|
|
|
5
|
|
26
|
use warnings; |
|
5
|
|
|
|
|
16
|
|
|
5
|
|
|
|
|
185
|
|
6
|
|
|
|
|
|
|
# use Data::Dumper; |
7
|
|
|
|
|
|
|
|
8
|
5
|
|
|
5
|
|
824
|
use Moo; |
|
5
|
|
|
|
|
17902
|
|
|
5
|
|
|
|
|
37
|
|
9
|
|
|
|
|
|
|
extends 'Data::Transpose::Validator::Base'; |
10
|
5
|
|
|
5
|
|
5393
|
use MooX::Types::MooseLike::Base qw(:all); |
|
5
|
|
|
|
|
9007
|
|
|
5
|
|
|
|
|
2367
|
|
11
|
5
|
|
|
5
|
|
1068
|
use namespace::clean; |
|
5
|
|
|
|
|
14538
|
|
|
5
|
|
|
|
|
39
|
|
12
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
our $VERSION = '0.02'; |
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
=head1 NAME |
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
Data::Transpose::PasswordPolicy - Perl extension to enforce password policy |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
=head1 SYNOPSIS |
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
use Data::Transpose::PasswordPolicy; |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
my %credentials = (username => "marco", |
24
|
|
|
|
|
|
|
password => "My.very.very.5strong.pzwd" |
25
|
|
|
|
|
|
|
); |
26
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
my $pv = Data::Transpose::PasswordPolicy->new(\%credentials) |
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
if (my $password = $pv->is_valid) { |
30
|
|
|
|
|
|
|
print "$password is OK"; |
31
|
|
|
|
|
|
|
} |
32
|
|
|
|
|
|
|
else { |
33
|
|
|
|
|
|
|
die $pv->error |
34
|
|
|
|
|
|
|
} |
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
=head1 DESCRIPTION |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
This module enforces the password policy, doing a number of checking. |
41
|
|
|
|
|
|
|
The author reccomends to use passphrases instead of password, using |
42
|
|
|
|
|
|
|
some special character (like punctuation) as separator, with 4-5 |
43
|
|
|
|
|
|
|
words in mixed case and with numbers as a good measure. |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
You can add the policy to the constructor, where C is the |
46
|
|
|
|
|
|
|
minimum password length, C is the maximum password and |
47
|
|
|
|
|
|
|
C is the minimum number of different characters in the |
48
|
|
|
|
|
|
|
password. Read below for C |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
By default all checkings are enabled. If you want to configure the |
51
|
|
|
|
|
|
|
policy, pass an hashref assigning to the disabled checking a true |
52
|
|
|
|
|
|
|
value. This will leave only the length checks in place, which you can |
53
|
|
|
|
|
|
|
tweak with the accessors. For example: |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
my %validate = ( username => "marco", |
59
|
|
|
|
|
|
|
password => "ciao", |
60
|
|
|
|
|
|
|
minlength => 10, |
61
|
|
|
|
|
|
|
maxlength => 50, |
62
|
|
|
|
|
|
|
patternlength => 4, |
63
|
|
|
|
|
|
|
mindiffchars => 5, |
64
|
|
|
|
|
|
|
disabled => { |
65
|
|
|
|
|
|
|
digits => 1, |
66
|
|
|
|
|
|
|
mixed => 1, |
67
|
|
|
|
|
|
|
} |
68
|
|
|
|
|
|
|
my $pv = Data::Transpose::PasswordPolicy->new(\%validate) |
69
|
|
|
|
|
|
|
$pv->is_valid ? "OK" : "not OK"; |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
See below for the list of the available checkings. |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
B: the purpose of this module is not to try to crack the |
75
|
|
|
|
|
|
|
password provided, but to set a policy for the passwords, which should |
76
|
|
|
|
|
|
|
have some minimum standards, and could be used on web services to stop |
77
|
|
|
|
|
|
|
users to set trivial password (without keeping the server busy for |
78
|
|
|
|
|
|
|
seconds while we check it). Nothing more. |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
=cut |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
=head1 METHODS |
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
=cut |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
=head2 new(%credentials) |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
Create a new Data::Transpose::PasswordPolicy object using the |
89
|
|
|
|
|
|
|
credentials provided to the constructor. |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
=cut |
92
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
has username => (is => 'rw', |
94
|
|
|
|
|
|
|
isa => Str); |
95
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
has password => (is => 'rw', |
97
|
|
|
|
|
|
|
isa => Str); |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
around password => \&_strip_space_on_around; |
100
|
|
|
|
|
|
|
around username => \&_strip_space_on_around; |
101
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
sub _strip_space_on_around { |
103
|
3550
|
|
|
3550
|
|
74422
|
my $orig = shift; |
104
|
3550
|
|
|
|
|
54977
|
my $ret = $orig->(@_); |
105
|
3550
|
50
|
|
|
|
18635
|
if (not defined $ret) { |
106
|
0
|
|
|
|
|
0
|
return ''; |
107
|
|
|
|
|
|
|
} |
108
|
|
|
|
|
|
|
else { |
109
|
3550
|
|
|
|
|
8758
|
$ret =~ s/^\s*//s; |
110
|
3550
|
|
|
|
|
18371
|
$ret =~ s/\s*$//s; |
111
|
3550
|
|
|
|
|
23588
|
return $ret; |
112
|
|
|
|
|
|
|
} |
113
|
|
|
|
|
|
|
} |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
has maxlength => (is => 'rw', |
117
|
|
|
|
|
|
|
isa => Int, |
118
|
|
|
|
|
|
|
default => sub { 255 }, |
119
|
|
|
|
|
|
|
); |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
has minlength => (is => 'rw', |
122
|
|
|
|
|
|
|
isa => Int, |
123
|
|
|
|
|
|
|
default => sub { 12 }, |
124
|
|
|
|
|
|
|
); |
125
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
has mindiffchars => (is => 'rw', |
128
|
|
|
|
|
|
|
isa => Int, |
129
|
|
|
|
|
|
|
default => sub { 6 }, |
130
|
|
|
|
|
|
|
); |
131
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
has patternlength => (is => 'rw', |
133
|
|
|
|
|
|
|
isa => Int, |
134
|
|
|
|
|
|
|
default => sub { 3 }, |
135
|
|
|
|
|
|
|
); |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
has disabled => (is => 'rw', |
138
|
|
|
|
|
|
|
isa => HashRef, |
139
|
|
|
|
|
|
|
default => sub { {} }); |
140
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
=head1 ACCESSORS |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
=head2 $obj->password($newpassword) |
145
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
Set and return the new password. If no argument is provided, returns |
147
|
|
|
|
|
|
|
the current. It will strip leading and trailing spaces. |
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
=head2 $obj->username($username) |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
Set and return the new username. If no argument is provided, returns |
152
|
|
|
|
|
|
|
the current. It will strip leading and trailing spaces. |
153
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
=head2 $obj->password_length |
155
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
It returns the length of the password; |
157
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
=cut |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
sub password_length { |
161
|
737
|
|
|
737
|
1
|
2828
|
my $self = shift; |
162
|
737
|
|
|
|
|
14237
|
return length($self->password); |
163
|
|
|
|
|
|
|
} |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
=head2 $obj->minlength |
166
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
Returns the minimum length required. If a numeric argument is |
168
|
|
|
|
|
|
|
provided, set that limit. Defaults to 255; |
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
=head2 $obj->maxlength |
171
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
As above, but for the maximum. Defaults to 12; |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
=head2 $obj->mindiffchars |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
As above, but set the minimum of different characters (to avoid things like |
177
|
|
|
|
|
|
|
00000000000000000ciao00000000000. |
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
Defaults to 6; |
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
=head2 $obj->patternlength |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
As above, but set the length of the common patterns we will search in |
184
|
|
|
|
|
|
|
the password, like "abcd", or "1234", or "asdf". By default it's 3, so |
185
|
|
|
|
|
|
|
a password which merely contains "abc" will be discarded. |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
This option can also be set in the constructor. |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
=head1 Internal algorithms |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
All the following methods operate on $obj->password and return the |
192
|
|
|
|
|
|
|
message of the error if something if not OK, while returning false if |
193
|
|
|
|
|
|
|
nothing suspicious was found. |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
=head2 password_length_ok |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
Check if the password is in the range of permitted lengths. Return |
198
|
|
|
|
|
|
|
undef if the validation passes, otherwise the arrayref with the error |
199
|
|
|
|
|
|
|
code and the error string. |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
=cut |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
sub password_length_ok { |
205
|
253
|
|
|
253
|
1
|
318
|
my $self = shift; |
206
|
253
|
100
|
100
|
|
|
393
|
if (($self->password_length >= $self->minlength) and |
207
|
|
|
|
|
|
|
($self->password_length <= $self->maxlength)) { |
208
|
245
|
|
|
|
|
2825
|
return undef; |
209
|
|
|
|
|
|
|
} else { |
210
|
8
|
|
50
|
|
|
2135
|
my $min = $self->minlength || 0; |
211
|
8
|
|
50
|
|
|
131
|
my $max = $self->maxlength || 0; |
212
|
8
|
|
50
|
|
|
1869
|
my $cur = $self->password_length || 0; |
213
|
8
|
100
|
|
|
|
25
|
if ($cur < $min) { |
214
|
7
|
|
|
|
|
62
|
return ["length" => "Wrong length (it should be long at least $min characters)"]; |
215
|
|
|
|
|
|
|
} |
216
|
|
|
|
|
|
|
else { |
217
|
1
|
|
|
|
|
6
|
return ["length" => "Password too long (max allowed $max)"]; |
218
|
|
|
|
|
|
|
} |
219
|
|
|
|
|
|
|
} |
220
|
|
|
|
|
|
|
} |
221
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
my %leetperms = ( |
226
|
|
|
|
|
|
|
'a' => qr{[4a]}, |
227
|
|
|
|
|
|
|
'b' => qr{[8b]}, |
228
|
|
|
|
|
|
|
'c' => "c", |
229
|
|
|
|
|
|
|
'd' => "d", |
230
|
|
|
|
|
|
|
'e' => qr{[3e]}, |
231
|
|
|
|
|
|
|
'f' => "f", |
232
|
|
|
|
|
|
|
'g' => "g", |
233
|
|
|
|
|
|
|
'h' => "h", |
234
|
|
|
|
|
|
|
'i' => qr{[1i]}, |
235
|
|
|
|
|
|
|
'j' => "j", |
236
|
|
|
|
|
|
|
'k' => "k", |
237
|
|
|
|
|
|
|
'l' => qr{[l1]}, |
238
|
|
|
|
|
|
|
'm' => "m", |
239
|
|
|
|
|
|
|
'n' => "n", |
240
|
|
|
|
|
|
|
'o' => qr{[0o]}, |
241
|
|
|
|
|
|
|
'p' => "p", |
242
|
|
|
|
|
|
|
'q' => "q", |
243
|
|
|
|
|
|
|
'r' => "r", |
244
|
|
|
|
|
|
|
's' => qr{[5s\$]}, |
245
|
|
|
|
|
|
|
't' => "t", |
246
|
|
|
|
|
|
|
'u' => "u", |
247
|
|
|
|
|
|
|
'v' => "v", |
248
|
|
|
|
|
|
|
'w' => "w", |
249
|
|
|
|
|
|
|
'x' => "x", |
250
|
|
|
|
|
|
|
'y' => "y", |
251
|
|
|
|
|
|
|
'z' => "z", |
252
|
|
|
|
|
|
|
'0' => qr{[o0]}, |
253
|
|
|
|
|
|
|
'1' => qr{[l1]}, |
254
|
|
|
|
|
|
|
'2' => "2", |
255
|
|
|
|
|
|
|
'3' => qr{[e3]}, |
256
|
|
|
|
|
|
|
'4' => qr{[4a]}, |
257
|
|
|
|
|
|
|
'5' => qr{[5s]}, |
258
|
|
|
|
|
|
|
'6' => "6", |
259
|
|
|
|
|
|
|
'7' => qr{[7t]}, |
260
|
|
|
|
|
|
|
'8' => qr{[8b]}, |
261
|
|
|
|
|
|
|
'9' => "9", |
262
|
|
|
|
|
|
|
); |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
my @toppassword = ( 'password', 'link', '1234', 'work', 'god', 'job', |
265
|
|
|
|
|
|
|
'angel', 'ilove', 'sex', 'jesus', 'connect', |
266
|
|
|
|
|
|
|
'f*ck', 'fu*k', 'monkey', 'master', 'bitch', 'dick', |
267
|
|
|
|
|
|
|
'micheal', 'jordan', 'dragon', 'soccer', 'killer', |
268
|
|
|
|
|
|
|
'4321', 'pepper', 'career', 'princess' ); |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
=head2 password_has_username |
272
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
Check if the password contains the username, even if obfuscated. |
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
Disable keyword: C |
276
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
=cut |
278
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
# check if the password doesn't contain the username |
281
|
|
|
|
|
|
|
sub password_has_username { |
282
|
220
|
|
|
220
|
1
|
231
|
my $self = shift; |
283
|
220
|
50
|
|
|
|
4296
|
return [ username => "Missing username" ] unless $self->username; |
284
|
|
|
|
|
|
|
|
285
|
220
|
|
|
|
|
4335
|
my $match = _leet_string_match($self->password, $self->username); |
286
|
220
|
100
|
|
|
|
422
|
if ($match) { |
287
|
2
|
|
|
|
|
11
|
return [ username => "Found username $match in password" ]; |
288
|
|
|
|
|
|
|
} else { |
289
|
|
|
|
|
|
|
return undef |
290
|
218
|
|
|
|
|
676
|
} |
291
|
|
|
|
|
|
|
} |
292
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
=head2 password_has_common_password |
294
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
Check if the password contains, even obfuscated, common password like |
296
|
|
|
|
|
|
|
"password" et similia. |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
Disable keyword: C |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
=cut |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
# check if the password is in the top ten :-) |
304
|
|
|
|
|
|
|
sub password_has_common_password { |
305
|
234
|
|
|
234
|
1
|
243
|
my $self = shift; |
306
|
234
|
|
|
|
|
193
|
my @found; |
307
|
234
|
|
|
|
|
4515
|
my $password = $self->password; |
308
|
234
|
|
|
|
|
484
|
foreach my $common (@toppassword) { |
309
|
6084
|
100
|
|
|
|
7886
|
if (_leet_string_match($password, $common)) { |
310
|
7
|
|
|
|
|
18
|
push @found, $common; |
311
|
|
|
|
|
|
|
} |
312
|
|
|
|
|
|
|
} |
313
|
234
|
100
|
|
|
|
454
|
if (@found) { |
314
|
|
|
|
|
|
|
# warn join(" ", @found) . "\n"; |
315
|
6
|
|
|
|
|
35
|
return [ common => "Found common password" ]; |
316
|
|
|
|
|
|
|
} |
317
|
|
|
|
|
|
|
else { |
318
|
228
|
|
|
|
|
879
|
return undef; |
319
|
|
|
|
|
|
|
} |
320
|
|
|
|
|
|
|
} |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
sub _leet_string_match { |
323
|
6304
|
|
|
6304
|
|
7193
|
my ($string, $match) = @_; |
324
|
6304
|
50
|
33
|
|
|
20423
|
return "Missing parameter" unless ($string and $match); |
325
|
|
|
|
|
|
|
|
326
|
6304
|
|
|
|
|
6660
|
my $lcstring = lc($string); # the password |
327
|
6304
|
|
|
|
|
5307
|
my $lcmatch = lc($match); # the check |
328
|
6304
|
|
|
|
|
11966
|
my @chars = split(//, $lcmatch); # split the match |
329
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
# for each character we look up the regexp or . |
331
|
6304
|
|
|
|
|
4618
|
my @regexps; |
332
|
6304
|
|
|
|
|
6688
|
foreach my $c (@chars) { |
333
|
32694
|
100
|
|
|
|
37750
|
if (exists $leetperms{$c}) { |
334
|
32226
|
|
|
|
|
38104
|
push @regexps, $leetperms{$c}; |
335
|
|
|
|
|
|
|
} else { |
336
|
468
|
|
|
|
|
505
|
push @regexps, "."; # unknown character |
337
|
|
|
|
|
|
|
} |
338
|
|
|
|
|
|
|
} |
339
|
|
|
|
|
|
|
# then we join it |
340
|
6304
|
|
|
|
|
9826
|
my $re = join("", @regexps); |
341
|
|
|
|
|
|
|
# and use it as re against the provided string |
342
|
|
|
|
|
|
|
# warn "checking $lcstring against $re\n"; |
343
|
6304
|
100
|
|
|
|
69261
|
if ($lcstring =~ m/$re/i) { |
344
|
|
|
|
|
|
|
# warn $re . "\n"; |
345
|
|
|
|
|
|
|
# return false if the re is present in the string |
346
|
9
|
|
|
|
|
42
|
return $lcmatch |
347
|
|
|
|
|
|
|
} else { |
348
|
6295
|
|
|
|
|
20994
|
return undef; |
349
|
|
|
|
|
|
|
} |
350
|
|
|
|
|
|
|
} |
351
|
|
|
|
|
|
|
|
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
|
354
|
|
|
|
|
|
|
=head2 password_has_enough_different_char |
355
|
|
|
|
|
|
|
|
356
|
|
|
|
|
|
|
Check if the password has enough different characters. |
357
|
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
Disable keyword: C |
359
|
|
|
|
|
|
|
|
360
|
|
|
|
|
|
|
=cut |
361
|
|
|
|
|
|
|
|
362
|
|
|
|
|
|
|
|
363
|
|
|
|
|
|
|
sub password_has_enough_different_char { |
364
|
234
|
|
|
234
|
1
|
281
|
my $self = shift; |
365
|
234
|
|
|
|
|
226
|
my %found; |
366
|
234
|
|
|
|
|
4489
|
my @chars = split //, $self->password; |
367
|
234
|
|
|
|
|
280
|
my %consecutives; |
368
|
234
|
|
|
|
|
261
|
my $previous = ""; |
369
|
234
|
|
|
|
|
347
|
foreach my $c (@chars) { |
370
|
9079
|
|
|
|
|
8652
|
$found{$c}++; |
371
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
# check previous char |
373
|
9079
|
100
|
|
|
|
12151
|
if ($previous eq $c) { |
374
|
305
|
|
|
|
|
383
|
$consecutives{$c}++; |
375
|
|
|
|
|
|
|
} |
376
|
9079
|
|
|
|
|
7665
|
$previous = $c; |
377
|
|
|
|
|
|
|
} |
378
|
|
|
|
|
|
|
# print Dumper(\%found); |
379
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
# check the number of chars |
381
|
234
|
|
|
|
|
372
|
my $totalchar = scalar(keys(%found)); |
382
|
234
|
100
|
|
|
|
4657
|
if ($totalchar <= $self->mindiffchars) { |
383
|
8
|
|
|
|
|
2039
|
return [ varchars => "Not enough different characters" ]; |
384
|
|
|
|
|
|
|
} |
385
|
|
|
|
|
|
|
|
386
|
226
|
|
|
|
|
2247
|
my %reportconsec; |
387
|
|
|
|
|
|
|
# check the consecutive chars; |
388
|
226
|
|
|
|
|
734
|
while (my ($k, $v) = each %consecutives) { |
389
|
221
|
100
|
|
|
|
1001
|
if ($v > 2) { |
390
|
7
|
|
|
|
|
33
|
$reportconsec{$k} = $v + 1; |
391
|
|
|
|
|
|
|
} |
392
|
|
|
|
|
|
|
} |
393
|
|
|
|
|
|
|
|
394
|
226
|
100
|
|
|
|
419
|
if (%reportconsec) { |
395
|
|
|
|
|
|
|
# we see if subtracting the number of total repetition, we are |
396
|
|
|
|
|
|
|
# still above the minimum chars. |
397
|
6
|
|
|
|
|
17
|
my $passwdlen = $self->password_length; |
398
|
6
|
|
|
|
|
22
|
foreach my $rep (values %reportconsec) { |
399
|
7
|
|
|
|
|
16
|
$passwdlen = $passwdlen - $rep; |
400
|
|
|
|
|
|
|
} |
401
|
6
|
100
|
|
|
|
159
|
if ($passwdlen < $self->minlength) { |
402
|
3
|
|
|
|
|
32
|
my $errstring = "Found too many repetitions, " |
403
|
|
|
|
|
|
|
. "lowering the effectivelength: " |
404
|
|
|
|
|
|
|
. (join(", ", (keys %reportconsec))); |
405
|
3
|
|
|
|
|
27
|
return [ varchars => $errstring ]; |
406
|
|
|
|
|
|
|
} |
407
|
|
|
|
|
|
|
} |
408
|
|
|
|
|
|
|
|
409
|
|
|
|
|
|
|
# given we have enough different characters, we check also there |
410
|
|
|
|
|
|
|
# are not some characters which are repeated too many times; |
411
|
|
|
|
|
|
|
# max dimension is 1/3 of the password |
412
|
223
|
|
|
|
|
448
|
my $maxrepeat = int($self->password_length / 3); |
413
|
|
|
|
|
|
|
# now get the hightest value; |
414
|
223
|
|
|
|
|
278
|
my $max = 0; |
415
|
223
|
|
|
|
|
647
|
foreach my $v (values %found) { |
416
|
4726
|
100
|
|
|
|
6949
|
$max = $v if ($v > $max); |
417
|
|
|
|
|
|
|
} |
418
|
223
|
100
|
|
|
|
373
|
if ($max > $maxrepeat) { |
419
|
3
|
|
|
|
|
62
|
return [ varchars => "Found too many repetitions" ]; |
420
|
|
|
|
|
|
|
} |
421
|
220
|
|
|
|
|
1831
|
return undef; |
422
|
|
|
|
|
|
|
} |
423
|
|
|
|
|
|
|
|
424
|
|
|
|
|
|
|
=head2 password_has_mixed_chars |
425
|
|
|
|
|
|
|
|
426
|
|
|
|
|
|
|
Check if the password has mixed cases |
427
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
Disable keyword: C |
429
|
|
|
|
|
|
|
|
430
|
|
|
|
|
|
|
=cut |
431
|
|
|
|
|
|
|
|
432
|
|
|
|
|
|
|
|
433
|
|
|
|
|
|
|
sub password_has_mixed_chars { |
434
|
224
|
|
|
224
|
1
|
229
|
my $self = shift; |
435
|
224
|
|
|
|
|
4242
|
my $pass = $self->password; |
436
|
224
|
100
|
100
|
|
|
1414
|
if (($pass =~ m/[a-z]/) and ($pass =~ m/[A-Z]/)) { |
437
|
|
|
|
|
|
|
return undef |
438
|
212
|
|
|
|
|
643
|
} else { |
439
|
12
|
|
|
|
|
60
|
return [ mixed => "No mixed case"]; |
440
|
|
|
|
|
|
|
} |
441
|
|
|
|
|
|
|
} |
442
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
=head2 password_has_specials |
444
|
|
|
|
|
|
|
|
445
|
|
|
|
|
|
|
Check if the password has non-word characters |
446
|
|
|
|
|
|
|
|
447
|
|
|
|
|
|
|
Disable keyword: C |
448
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
=cut |
450
|
|
|
|
|
|
|
|
451
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
sub password_has_specials { |
453
|
234
|
|
|
234
|
1
|
254
|
my $self = shift; |
454
|
234
|
100
|
|
|
|
4557
|
if ($self->password =~ m/[\W_]/) { |
455
|
|
|
|
|
|
|
return undef |
456
|
224
|
|
|
|
|
682
|
} else { |
457
|
10
|
|
|
|
|
46
|
return [ specials => "No special characters" ]; |
458
|
|
|
|
|
|
|
} |
459
|
|
|
|
|
|
|
} |
460
|
|
|
|
|
|
|
|
461
|
|
|
|
|
|
|
=head2 password_has_digits |
462
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
Check if the password has digits |
464
|
|
|
|
|
|
|
|
465
|
|
|
|
|
|
|
Disable keyword: C |
466
|
|
|
|
|
|
|
|
467
|
|
|
|
|
|
|
=cut |
468
|
|
|
|
|
|
|
|
469
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
sub password_has_digits { |
471
|
224
|
|
|
224
|
1
|
229
|
my $self = shift; |
472
|
224
|
100
|
|
|
|
4179
|
if ($self->password =~ m/\d/) { |
473
|
|
|
|
|
|
|
return undef |
474
|
215
|
|
|
|
|
640
|
} else { |
475
|
9
|
|
|
|
|
44
|
return [ digits => "No digits in the password" ]; |
476
|
|
|
|
|
|
|
} |
477
|
|
|
|
|
|
|
} |
478
|
|
|
|
|
|
|
|
479
|
|
|
|
|
|
|
=head2 password_has_letters |
480
|
|
|
|
|
|
|
|
481
|
|
|
|
|
|
|
Check if the password has letters |
482
|
|
|
|
|
|
|
|
483
|
|
|
|
|
|
|
Disable keyword: C |
484
|
|
|
|
|
|
|
|
485
|
|
|
|
|
|
|
=cut |
486
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
sub password_has_letters { |
488
|
234
|
|
|
234
|
1
|
251
|
my $self = shift; |
489
|
234
|
100
|
|
|
|
4352
|
if ($self->password =~ m/[a-zA-Z]/) { |
490
|
|
|
|
|
|
|
return undef |
491
|
231
|
|
|
|
|
650
|
} else { |
492
|
3
|
|
|
|
|
13
|
return [letters => "No letters in the password" ]; |
493
|
|
|
|
|
|
|
} |
494
|
|
|
|
|
|
|
} |
495
|
|
|
|
|
|
|
|
496
|
|
|
|
|
|
|
=head2 password_has_patterns |
497
|
|
|
|
|
|
|
|
498
|
|
|
|
|
|
|
Check if the password contains usual patterns like 12345, abcd, or |
499
|
|
|
|
|
|
|
asdf (like in the qwerty keyboard). |
500
|
|
|
|
|
|
|
|
501
|
|
|
|
|
|
|
Disable keyword: C |
502
|
|
|
|
|
|
|
|
503
|
|
|
|
|
|
|
=cut |
504
|
|
|
|
|
|
|
|
505
|
|
|
|
|
|
|
my @patterns = ( |
506
|
|
|
|
|
|
|
[ qw/1 2 3 4 5 6 7 8 9 0/ ], |
507
|
|
|
|
|
|
|
[ ("a" .. "z") ], |
508
|
|
|
|
|
|
|
[ qw/q w e r t y u i o p/ ], |
509
|
|
|
|
|
|
|
[ qw/a s d f g h j k l/ ], |
510
|
|
|
|
|
|
|
[ qw/z x c v b n m/ ]); |
511
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
sub password_has_patterns { |
513
|
237
|
|
|
237
|
1
|
266
|
my $self = shift; |
514
|
237
|
|
|
|
|
4412
|
my $password = lc($self->password); |
515
|
237
|
|
|
|
|
281
|
my @found; |
516
|
237
|
|
|
|
|
4092
|
my $range = $self->patternlength - 1; |
517
|
237
|
|
|
|
|
4112
|
foreach my $row (@patterns) { |
518
|
1185
|
|
|
|
|
3095
|
my @pat = @$row; |
519
|
|
|
|
|
|
|
# we search a pattern of 3 consecutive keys, maybe 4 is reasonable enough |
520
|
1185
|
|
|
|
|
2272
|
for (my $i = 0; $i <= ($#pat - $range); $i++) { |
521
|
12269
|
|
|
|
|
9658
|
my $to = $i + $range; |
522
|
12269
|
|
|
|
|
13943
|
my $substring = join("", @pat[$i..$to]); |
523
|
12269
|
100
|
|
|
|
31379
|
if (index($password, $substring) >= 0) { |
524
|
11
|
|
|
|
|
30
|
push @found, $substring; |
525
|
|
|
|
|
|
|
} |
526
|
|
|
|
|
|
|
} |
527
|
|
|
|
|
|
|
} |
528
|
237
|
100
|
|
|
|
433
|
if (@found) { |
529
|
8
|
|
|
|
|
25
|
my $errstring = "Found common patterns: " . join(", ", @found); |
530
|
8
|
|
|
|
|
43
|
return [ patterns => $errstring ]; |
531
|
|
|
|
|
|
|
} else { |
532
|
229
|
|
|
|
|
839
|
return undef; |
533
|
|
|
|
|
|
|
} |
534
|
|
|
|
|
|
|
} |
535
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
=head1 Main methods |
538
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
=head2 $obj->is_valid |
540
|
|
|
|
|
|
|
|
541
|
|
|
|
|
|
|
Return the password if matches the policy or a false value if not. |
542
|
|
|
|
|
|
|
|
543
|
|
|
|
|
|
|
For convenience, this method can accept the password to validate as |
544
|
|
|
|
|
|
|
argument, which will overwrite the one provided with the C |
545
|
|
|
|
|
|
|
method (if it was set). |
546
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
=cut |
548
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
|
550
|
|
|
|
|
|
|
|
551
|
|
|
|
|
|
|
sub is_valid { |
552
|
254
|
|
|
254
|
1
|
14550
|
my $self = shift; |
553
|
254
|
|
|
|
|
317
|
my $password = shift; |
554
|
254
|
100
|
66
|
|
|
764
|
if (defined $password and $password ne "") { |
555
|
15
|
|
|
|
|
340
|
$self->password($password); |
556
|
|
|
|
|
|
|
} |
557
|
254
|
100
|
|
|
|
5143
|
unless ($self->password) { |
558
|
1
|
|
|
|
|
7
|
$self->error([missing => "Password is missing"]); |
559
|
1
|
|
|
|
|
5
|
return undef; |
560
|
|
|
|
|
|
|
} |
561
|
|
|
|
|
|
|
# reset the errors, we are going to do the checks anew; |
562
|
253
|
|
|
|
|
840
|
$self->reset_errors; |
563
|
|
|
|
|
|
|
|
564
|
|
|
|
|
|
|
|
565
|
|
|
|
|
|
|
|
566
|
|
|
|
|
|
|
# To disable this, set the minimum to 1 and the max |
567
|
|
|
|
|
|
|
# to 255, but it makes no sense. |
568
|
253
|
|
|
|
|
9994
|
$self->error($self->password_length_ok); |
569
|
|
|
|
|
|
|
|
570
|
253
|
100
|
|
|
|
687
|
unless ($self->is_disabled("specials")) { |
571
|
234
|
|
|
|
|
3629
|
$self->error($self->password_has_specials); |
572
|
|
|
|
|
|
|
} |
573
|
|
|
|
|
|
|
|
574
|
253
|
100
|
|
|
|
626
|
unless ($self->is_disabled("digits")) { |
575
|
224
|
|
|
|
|
1461
|
$self->error($self->password_has_digits); |
576
|
|
|
|
|
|
|
} |
577
|
|
|
|
|
|
|
|
578
|
253
|
100
|
|
|
|
693
|
unless ($self->is_disabled("letters")) { |
579
|
234
|
|
|
|
|
1600
|
$self->error($self->password_has_letters); |
580
|
|
|
|
|
|
|
} |
581
|
|
|
|
|
|
|
|
582
|
253
|
100
|
|
|
|
554
|
unless ($self->is_disabled("username")) { |
583
|
220
|
|
|
|
|
1381
|
$self->error($self->password_has_username); |
584
|
|
|
|
|
|
|
} |
585
|
|
|
|
|
|
|
|
586
|
253
|
100
|
|
|
|
698
|
unless ($self->is_disabled("common")) { |
587
|
234
|
|
|
|
|
1567
|
$self->error($self->password_has_common_password); |
588
|
|
|
|
|
|
|
} |
589
|
|
|
|
|
|
|
|
590
|
253
|
100
|
|
|
|
592
|
unless ($self->is_disabled("varchars")) { |
591
|
234
|
|
|
|
|
1646
|
$self->error($self->password_has_enough_different_char); |
592
|
|
|
|
|
|
|
} |
593
|
|
|
|
|
|
|
|
594
|
253
|
100
|
|
|
|
639
|
unless ($self->is_disabled("mixed")) { |
595
|
224
|
|
|
|
|
1597
|
$self->error($self->password_has_mixed_chars); |
596
|
|
|
|
|
|
|
} |
597
|
|
|
|
|
|
|
|
598
|
253
|
100
|
|
|
|
647
|
unless ($self->is_disabled("patterns")) { |
599
|
237
|
|
|
|
|
1557
|
$self->error($self->password_has_patterns) |
600
|
|
|
|
|
|
|
} |
601
|
|
|
|
|
|
|
|
602
|
253
|
100
|
|
|
|
729
|
if ($self->error) { |
603
|
28
|
|
|
|
|
158
|
return undef; |
604
|
|
|
|
|
|
|
} else { |
605
|
225
|
|
|
|
|
4211
|
return $self->password; |
606
|
|
|
|
|
|
|
} |
607
|
|
|
|
|
|
|
} |
608
|
|
|
|
|
|
|
|
609
|
|
|
|
|
|
|
|
610
|
|
|
|
|
|
|
=head2 $obj->error |
611
|
|
|
|
|
|
|
|
612
|
|
|
|
|
|
|
With argument, set the error. Without, return the errors found in the |
613
|
|
|
|
|
|
|
password. |
614
|
|
|
|
|
|
|
|
615
|
|
|
|
|
|
|
In list context, we pass the array with the error codes and the strings. |
616
|
|
|
|
|
|
|
In scalar context, we return the concatenated error strings. |
617
|
|
|
|
|
|
|
|
618
|
|
|
|
|
|
|
Inherited from Data::Transpose::Validator::Base; |
619
|
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
=cut |
621
|
|
|
|
|
|
|
|
622
|
|
|
|
|
|
|
=head2 error_codes |
623
|
|
|
|
|
|
|
|
624
|
|
|
|
|
|
|
Return a list of the error codes found in the password. The error |
625
|
|
|
|
|
|
|
codes match the options. (e.g. C, C). |
626
|
|
|
|
|
|
|
|
627
|
|
|
|
|
|
|
If you want the verbose string, you need the C method. |
628
|
|
|
|
|
|
|
|
629
|
|
|
|
|
|
|
=cut |
630
|
|
|
|
|
|
|
|
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
|
633
|
|
|
|
|
|
|
|
634
|
|
|
|
|
|
|
=head2 $obj->reset_errors |
635
|
|
|
|
|
|
|
|
636
|
|
|
|
|
|
|
Clear the object from previous errors, in case you want to reuse it. |
637
|
|
|
|
|
|
|
|
638
|
|
|
|
|
|
|
=cut |
639
|
|
|
|
|
|
|
|
640
|
|
|
|
|
|
|
=head2 $obj->disable("mixed", "letters", "digits", [...]) |
641
|
|
|
|
|
|
|
|
642
|
|
|
|
|
|
|
Disable the checking(s) passed as list of strings. |
643
|
|
|
|
|
|
|
|
644
|
|
|
|
|
|
|
=cut |
645
|
|
|
|
|
|
|
|
646
|
|
|
|
|
|
|
sub disable { |
647
|
10
|
|
|
10
|
1
|
4227
|
my $self = shift; |
648
|
10
|
|
|
|
|
29
|
$self->_enable_or_disable_check("disable", @_); |
649
|
|
|
|
|
|
|
} |
650
|
|
|
|
|
|
|
|
651
|
|
|
|
|
|
|
=head2 $obj->enable("mixed", "letters", [...]) |
652
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
Same as above, but enable the checking |
654
|
|
|
|
|
|
|
|
655
|
|
|
|
|
|
|
=cut |
656
|
|
|
|
|
|
|
|
657
|
|
|
|
|
|
|
|
658
|
|
|
|
|
|
|
sub enable { |
659
|
10
|
|
|
10
|
1
|
27
|
my $self = shift; |
660
|
10
|
|
|
|
|
21
|
$self->_enable_or_disable_check("enable", @_); |
661
|
|
|
|
|
|
|
} |
662
|
|
|
|
|
|
|
|
663
|
|
|
|
|
|
|
sub _enable_or_disable_check { |
664
|
20
|
|
|
20
|
|
23
|
my $self = shift; |
665
|
20
|
|
|
|
|
19
|
my $action = shift; |
666
|
20
|
|
|
|
|
45
|
my @args = @_; |
667
|
20
|
|
|
|
|
24
|
my $set = 0; |
668
|
20
|
50
|
66
|
|
|
101
|
die "Wrong usage! internal only!\n" unless (($action eq 'enable') or |
669
|
|
|
|
|
|
|
($action eq 'disable')); |
670
|
|
|
|
|
|
|
|
671
|
20
|
50
|
|
|
|
51
|
if (@args) { |
672
|
20
|
|
|
|
|
30
|
foreach my $what (@args) { |
673
|
20
|
|
|
|
|
41
|
$self->_get_or_set_disable($what, $action); |
674
|
|
|
|
|
|
|
} |
675
|
|
|
|
|
|
|
} |
676
|
|
|
|
|
|
|
} |
677
|
|
|
|
|
|
|
|
678
|
|
|
|
|
|
|
=head2 $obj->is_disabled("checking") |
679
|
|
|
|
|
|
|
|
680
|
|
|
|
|
|
|
Return true if the checking is disable. |
681
|
|
|
|
|
|
|
|
682
|
|
|
|
|
|
|
=cut |
683
|
|
|
|
|
|
|
|
684
|
|
|
|
|
|
|
sub is_disabled { |
685
|
2024
|
|
|
2024
|
1
|
2640
|
my $self = shift; |
686
|
2024
|
|
|
|
|
1900
|
my $check = shift; |
687
|
2024
|
|
|
|
|
2804
|
return $self->_get_or_set_disable($check); |
688
|
|
|
|
|
|
|
} |
689
|
|
|
|
|
|
|
|
690
|
|
|
|
|
|
|
sub _get_or_set_disable { |
691
|
2044
|
|
|
2044
|
|
2044
|
my ($self, $what, $action) = @_; |
692
|
2044
|
50
|
|
|
|
3281
|
return undef unless $what; |
693
|
2044
|
100
|
|
|
|
2942
|
unless ($action) { |
694
|
2024
|
|
|
|
|
34429
|
return $self->disabled->{$what} |
695
|
|
|
|
|
|
|
} |
696
|
20
|
100
|
|
|
|
48
|
if ($action eq 'enable') { |
|
|
50
|
|
|
|
|
|
697
|
10
|
|
|
|
|
214
|
$self->disabled->{$what} = 0; |
698
|
|
|
|
|
|
|
} |
699
|
|
|
|
|
|
|
elsif ($action eq 'disable') { |
700
|
10
|
|
|
|
|
234
|
$self->disabled->{$what} = 1; |
701
|
|
|
|
|
|
|
} |
702
|
|
|
|
|
|
|
else { |
703
|
0
|
|
|
|
|
0
|
die "Wrong action!\n" |
704
|
|
|
|
|
|
|
} |
705
|
20
|
|
|
|
|
864
|
return $self->disabled->{$what}; |
706
|
|
|
|
|
|
|
} |
707
|
|
|
|
|
|
|
|
708
|
|
|
|
|
|
|
|
709
|
|
|
|
|
|
|
1; |
710
|
|
|
|
|
|
|
|
711
|
|
|
|
|
|
|
__END__ |