File Coverage

blib/lib/Data/Password/Check.pm
Criterion Covered Total %
statement 109 126 86.5
branch 49 66 74.2
condition 13 24 54.1
subroutine 16 16 100.0
pod 3 3 100.0
total 190 235 80.8


line stmt bran cond sub pod time code
1             package Data::Password::Check;
2 9     9   328131 use strict;
  9         25  
  9         384  
3 9     9   50 use warnings;
  9         18  
  9         981  
4 9     9   86 use Carp;
  9         22  
  9         19585  
5              
6             our $VERSION = '0.08';
7              
8             =head1 NAME
9              
10             Data::Password::Check - sanity check passwords
11              
12             =head1 DESCRIPTION
13              
14             Users can be lazy. If you're a perl programmer this is a good thing. If you're
15             choosing a password this is a bad thing.
16              
17             This module performs some sanity checks on passwords. Details on checks than
18             can be performed are described below.
19              
20             =head1 SYNOPSIS
21              
22             Basic use of the module is as follows:
23              
24             use Data::Password::Check;
25              
26             # check a password
27             my $pwcheck = Data::Password::Check->check({
28             'password' => $some_password
29             });
30              
31             # did we have any errors?
32             if ($pwcheck->has_errors) {
33             # print the errors
34             print(
35             join("\n", @{ $pwcheck->error_list }),
36             "\n"
37             );
38             }
39              
40             =cut
41              
42             =head1 PUBLIC METHODS
43              
44             These methods are publically available. Use them to your heart's content.
45              
46             =head2 check($proto,$options)
47              
48             This is the main function for this module. You must pass one mandatory value in
49             the $options hash-reference - a password:
50              
51             # check a password
52             $result = Data::Password::Check->check({'password' => $pwd_to_check});
53              
54             There are other options that may be passed to invoke further password tests if
55             required:
56              
57             =over 4
58              
59             =item * tests
60              
61             set this to a list of test names to replace the list of tests performed by the module
62              
63             e.g. tests =E [ 'length' ] will make the module perfoem the length check only
64              
65             =item * tests_append
66              
67             set this to a list of additional tests to perform. This is useful if you want
68             to call more tests than are in the default list, or to include your own tests
69             when inheriting from this module.
70              
71             e.g. test =E [ 'mytest1', 'mytest2' ] will make the module perform two
72             extra tests (assuming they exist) mytest1 and mytest2.
73              
74             =back
75              
76             =cut
77             sub check($$) {
78 51     51 1 14972 my ($proto, $options) = @_;
79 51         82 my ($self, $class);
80              
81 51   33     252 $class = ref($proto) || $proto;
82 51         90 $self = {};
83 51         142 bless $self, $class;
84              
85             # make sure $options is a hash-reference
86 51 50       171 unless (ref($options) eq 'HASH') {
87 0         0 Carp::carp("You need to pass a hash-reference of options to check()");
88 0         0 return undef;
89             }
90              
91             # make sure we at least have a password value
92 51 50       171 unless (exists $options->{'password'}) {
93 0         0 Carp::carp("You need to supply a password to check()!");
94 0         0 return undef;
95             }
96              
97             # store the password so it's easier to refer to
98             # (i.e. $self->{'password'} rather than $self->{'options'}{'password'})
99 51         170 $self->{'password'} = $options->{'password'};
100              
101             # make a copy of the incomong options
102 51         91 $self->{'options'} = $options;
103              
104             # perform the password checks
105 51         242 $self->_do_checks;
106              
107 51         168 return $self;
108             }
109              
110             =head2 has_errors($class)
111              
112             This function is used to determine if there were any errors found while sanity
113             checking the supplied password. It does not return the errors themselves.
114              
115             Returns B<1> if there were errors, B<0> otherwise
116              
117             =cut
118             sub has_errors($) {
119 155     155 1 18405 my ($self) = @_;
120 155   66     942 return (exists $self->{'_error_count'} and $self->{'_error_count'} > 0);
121             }
122              
123             =head2 error_list($class)
124              
125             This function returns an array-reference to a list of the error messages.
126             If there are no errors B is returned.
127              
128             =cut
129             sub error_list($) {
130 24     24 1 100 my ($self) = @_;
131 24 50       58 if ($self->has_errors) {
132 24         188 return $self->{'_errors'};
133             }
134              
135 0         0 return undef;
136             }
137              
138              
139             =head1 AVAILABLE CHECKS
140              
141             By default the module will perform all checks listed below. You can limit the
142             number of checks by passing a list of desired tests via the B option
143             when calling check(). e.g.
144              
145             Data::Password::Check->check({
146             ...
147             'tests' => [ 'length' ], # check only that the password meets a minimum-length requirement
148             ...
149             });
150              
151             =cut
152              
153             =head2 alphanumeric_only
154              
155             Make sure the password only contains a-z, A-Z and 0-9 characters.
156              
157             =cut
158             sub _check_alphanumeric_only($) {
159 3     3   6 my ($self) = @_;
160              
161             # make sure the password only contains alphanumeric characters
162 3 100       34 unless ($self->{'password'} =~ /^[[:alnum:]]+$/) {
163 2         33 $self->_add_error("Your password may only contain alphanumeric characters (A-Z, a-z and 0-9)");
164             }
165             }
166              
167             =head2 alphanumeric
168              
169             Make sure the password contains one of each from the following sets: a-z, A-Z and 0-9
170              
171             =cut
172             sub _check_alphanumeric($) {
173 7     7   10 my ($self) = @_;
174              
175             # make sure the password contains one lower case and one uppercase character, and one digit - at least
176             # tr// seems the best way (at the moment) to check this requirement
177 7 100 66     892 unless (
      33        
178             ($self->{'password'} =~ tr/a-z//) and
179             ($self->{'password'} =~ tr/A-Z//) and
180             ($self->{'password'} =~ tr/0-9//) ) {
181 3         8 $self->_add_error("Your password must contain mixed-case letters and numbers");
182             }
183             }
184              
185             =head2 length
186              
187             Make sure the password it at least 6 characters long. If B was passed
188             as an option to check(), this value will be used instead, assuming it's a
189             positive integer.
190              
191             =cut
192             sub _check_length($) {
193 13     13   23 my ($self) = @_;
194 13         17 my $min_length = 6;
195              
196             # does the user want a different length
197 13 100 100     826 if (exists $self->{'options'}{'min_length'} and not defined $self->{'options'}{'min_length'}) {
    100 66        
198             # issue a warning
199 1         143 Carp::cluck("min_length argument must be a defined value");
200             # return undef
201 1         261 return undef;
202             }
203             elsif (exists $self->{'options'}{'min_length'} and defined $self->{'options'}{'min_length'}) {
204             # is it a positive, on-zero, integer?
205 7 100       42 if ($self->{'options'}{'min_length'} =~ /^[1-9]\d*$/) {
206 4         8 $min_length = $self->{'options'}{'min_length'};
207             }
208             else {
209             # issue a warning
210 3         531 Carp::cluck("min_length argument [$self->{'options'}{'min_length'} isn't a positive, non-zero, integer");
211             # return undef
212 3         535 return undef;
213             }
214             }
215              
216             # if password is undefined, set it to '', so we aren't comparing undef with anything
217 9 100       32 unless (defined $self->{'password'}) {
218 1         2 $self->{'password'} = '';
219             }
220              
221             # now we can check that the password meets the minimum length requirement
222 9 100       30 if (length($self->{'password'}) >= $min_length) {
223 6         33 return 1;
224             }
225             else {
226             # store a failure message
227 3         17 $self->_add_error("The password must be at least $min_length characters");
228             }
229             }
230              
231             =head2 mixed_case
232              
233             Make sure the password is mixes case, i.e. not all lower case, nor all upper case
234              
235             =cut
236             sub _check_mixed_case($) {
237 12     12   20 my ($self) = @_;
238              
239             # does the password contain at least one lowercase and one uppercase character?
240 12 100       112 unless ($self->{'password'} =~ /(?:[A-Z].*[a-z]|[a-z].*[A-Z])/) {
241 8         25 $self->_add_error("Your password must contain a mixture of lower and upper-case letters");
242             }
243             }
244              
245             =head2 diverse_characters
246              
247             Make sure the password is contains a diversity of character group types
248             (uppercase, lower case, digits, symbols). By default, at least one character
249             group must be present in the password (which any password will satisfy -
250             override this to invoke the test). If B was passed
251             as an option to check(), this value will be used instead.
252              
253             =cut
254             sub _check_diverse_characters($) {
255 10     10   12 my ($self) = @_;
256 10         10 my $diversity_required = 1;
257             # does the user want a different diversity?
258 10 50 33     688 if (exists $self->{'options'}{'diversity_required'} and not defined $self->{'options'}{'diversity_required'}) {
    50 33        
259             # issue a warning
260 0         0 Carp::cluck("diversity_required argument must be a defined value");
261 0         0 return undef;
262             }
263             elsif (exists $self->{'options'}{'diversity_required'} and defined $self->{'options'}{'diversity_required'}) {
264             # is it in range?
265 10 50       48 if ($self->{'options'}{'diversity_required'} =~ /^[1-4]\d*$/) {
266 10         20 $diversity_required = $self->{'options'}{'diversity_required'};
267             }
268             else {
269             # issue a warning
270 0         0 Carp::cluck("diversity_required argument [$self->{'options'}{'diversity_required'} isn't in the range 1-4");
271 0         0 return undef;
272             }
273             }
274            
275 10         11 my $group_count = 0;
276 10         14 foreach my $pattern (qw([A-Z] [a-z] [0-9] [^A-Za-z0-9]))
277             {
278 40 100       358 if ($self->{'password'} =~ /$pattern/) {
279 18         33 $group_count++;
280             }
281             }
282            
283             # Are enough character groups used to satisfy diversity requirements?
284 10 100       51 if ($group_count < $diversity_required) {
285 3         11 $self->_add_error("Your password must contain a good mix of character types, from at least $diversity_required of the following categories: Uppercase letters, lowercase letters, numeral, symbols.");
286             }
287             }
288              
289             =head2 silly
290              
291             Make sure the password isn't a known silly word (e.g 'password' is a bad choice
292             for a password).
293              
294             The default list contains I, and I only. You may choose to
295             replace this list of words or to add your own to the end of the list.
296              
297             If you wish to B the list of silly-words, you should pass them in via
298             the options when calling check(), as 'silly_words'. e.g.
299              
300             Data::Password::Check->check({
301             ...
302             'silly_words' => [ 'my', 'silly', 'words' ],
303             ...
304             });
305              
306             If you would like to add words to the existing list, you should pass them in
307             via the 'silly_words_append' option when calling check(). e.g.
308              
309             Data::Password::Check->check({
310             ...
311             'silly_words_append' => [ 'more', 'silly', 'words' ],
312             ...
313             });
314              
315             All matching is case-insensitive, and if you choose to append words, duplicates
316             will be omitted.
317              
318             =cut
319             sub _check_silly($) {
320 13     13   21 my ($self) = @_;
321             # default words we don't want people to use as passwords
322 13         33 my @silly_words = qw{
323             password
324             qwerty
325             };
326             # does the user want to REPLACE the current list of words
327 13 100       61 if (exists $self->{'options'}{'silly_words'}) {
328             # is it an array-ref?
329 6 50       17 if (ref($self->{'options'}{'silly_words'}) eq 'ARRAY') {
330             # override the default checks
331 6         9 @silly_words = @{ $self->{'options'}{'silly_words'} };
  6         22  
332             }
333             else {
334 0         0 Carp::carp("The 'silly_words' option must be an array-reference. Continuing with default list.");
335             }
336             }
337              
338             # does the user want to ADD to the existing list of word
339 13 100       41 if (exists $self->{'options'}{'silly_words_append'}) {
340             # is it an array-ref?
341 2 50       9 if (ref($self->{'options'}{'silly_words_append'}) eq 'ARRAY') {
342             # push the words onto the end of the list
343             # make sure we don't already have the word
344 2         3 foreach my $append (@{ $self->{'options'}{'silly_words_append'} }) {
  2         7  
345 6 50       36 unless ( grep { /^$append$/ } @silly_words ) {
  18         130  
346 6         13 push @silly_words, $append;
347             }
348             }
349             }
350             else {
351 0         0 Carp::carp("The 'silly_words' option must be an array-reference. Continuing with default list.");
352             }
353             }
354              
355             # now we loop through the silly_words and make sure our password doesn't match them
356 13         28 foreach my $silly (@silly_words) {
357             # do a case-insensitive match, but look for the whole string
358 35 100       527 if ($self->{'password'} =~ /^$silly$/i) {
359 6         30 $self->_add_error("You may not use '$self->{'password'}' as your password");
360             }
361             }
362             }
363              
364             =head2 repeated
365              
366             Make sure the password isn't a single character repeated, e.g. 'aaaaaaaaaa'.
367              
368             =cut
369             sub _check_repeated($) {
370 9     9   14 my ($self) = @_;
371              
372             # is the password made up of the same character repeated?
373 9 100       92 if ($self->{'password'} =~ /^(.)\1+$/) {
374 3         11 $self->_add_error("You cannot use a single repeated character as a password");
375             }
376             }
377              
378              
379             =head1 PRIVATE METHODS
380              
381             These methods are private to this module. If you choose to use them outside the
382             module, all bets are off.
383              
384             =head2 _do_checks($self)
385              
386             This function calls each required test in turn. It's an internal function
387             called within check().
388              
389             =cut
390             sub _do_checks($) {
391 51     51   81 my ($self) = @_;
392 51         64 my (@checks, $fn, $custom_checks);
393              
394             # the list of checks to make
395 51         131 @checks = qw(
396             length
397             mixed_case
398             silly
399             repeated
400             );
401             # custom_checks defaults to false
402 51         74 $custom_checks = 0;
403              
404             # allow the user to override the list of checks
405             # we require the 'tests' option to exist, and to be an array-reference
406 51 100       161 if (exists $self->{'options'}{'tests'}) {
407 46 50       182 if (ref($self->{'options'}{'tests'}) eq 'ARRAY') {
408             # override the default checks
409 46         58 @checks = @{ $self->{'options'}{'tests'} };
  46         137  
410             # set the custom_checks flag
411 46         82 $custom_checks = 1;
412             }
413             else {
414 0         0 Carp::carp("The 'tests' option must be an array-reference. Continuing with default tests.");
415             }
416             }
417              
418             # allow the user to override the list of checks
419             # we require the 'tests' option to exist, and to be an array-reference
420 51 100       148 if (exists $self->{'options'}{'append_tests'}) {
421 1 50       6 if (ref($self->{'options'}{'append_tests'}) eq 'ARRAY') {
422             # override the default checks
423 1         2 @checks = (@checks, @{ $self->{'options'}{'append_tests'} });
  1         6  
424             # set the custom_checks flag
425 1         3 $custom_checks = 1;
426             }
427             else {
428 0         0 Carp::carp("The 'append_tests' option must be an array-reference. Continuing with default tests.");
429             }
430             }
431              
432             # loop through the checks we would like to do
433 51         96 foreach my $test (@checks) {
434             # set the name of the function we'd like to call
435 67         153 my $fn = "_check_${test}";
436             # if we can run the function, do so
437 67 50       357 if ($self->can("_check_${test}")) {
438 67 100       220 unless (defined $self->$fn) {
439             # make a note that we skipped the test
440 4         8 push @{ $self->{'skipped_tests'} }, $test;
  4         12  
441 4 50       23 Carp::carp("skipped test '$test' due to errors") if $self->{'DEBUG'};
442             };
443             }
444             # otherwise warn that we're trying to call something
445             # that we can't find
446             else {
447             # warn or carp, depending on whether we've got a custom
448             # list of tests
449 0 0       0 if ($custom_checks) {
450 0         0 Carp::carp("The are no password checks available for '$test'");
451             }
452             else {
453 0         0 warn "no such password check function: $fn()";
454             }
455             }
456             }
457             }
458              
459             =head2 _add_error($class,$message)
460              
461             This function is used to add an error message to the internal store.
462             The errors can later be retrieved using the B method.
463              
464             =cut
465             sub _add_error($$) {
466 28     28   52 my ($self, $message) = @_;
467              
468             # increase the count of errors we've added
469 28         51 $self->{'_error_count'} ++;
470              
471             # add the error message to a list of messages
472 28         63 push @{ $self->{'_errors'} }, $message;
  28         240  
473             }
474              
475             =head2 _skipped_test($class,$testname)
476              
477             This function exists so that it's possible to work out if a test was skipped
478             because "something went wrong" - usually because of an invalid option passed in
479             via the check() options.
480              
481             This function was written to enable some tests in the "make test" phase of
482             installing the module.
483              
484             =cut
485             sub _skipped_test($$) {
486 4     4   32 my ($self, $testname) = @_;
487              
488             # do we have a list of skipped tests?
489 4 50       13 if (exists $self->{'skipped_tests'}) {
490             # does $testname exist in the list?
491 4 50       6 if (grep { /^$testname$/ } @{ $self->{'skipped_tests'} }) {
  4         49  
  4         11  
492 4         18 return 1;
493             }
494             }
495              
496             # no indication that we skipped the test
497 0           return 0;
498             }
499              
500             =head1 AUTHOR
501              
502             Chisel Wright C<< >>
503              
504             =head1 CONTRIBUTORS
505              
506             Dermot McNally C<< CPANID: DERMOT >>
507              
508             =head1 PROJECT HOMEPAGE
509              
510             This project can be found at BerliOS:
511             L
512              
513             =head1 COPYRIGHT AND LICENCE
514              
515             Copyright (C) 2005-2007 by Chisel Wright
516              
517             This library is free software; you can redistribute it and/or modify
518             it under the same terms as Perl itself, either Perl version 5.8.2 or,
519             at your option, any later version of Perl 5 you may have available.
520              
521             =cut
522              
523             # be true
524             1;
525              
526             __END__