File Coverage

blib/lib/Data/Password/Filter.pm
Criterion Covered Total %
statement 106 120 88.3
branch 26 38 68.4
condition 7 18 38.8
subroutine 19 20 95.0
pod 3 4 75.0
total 161 200 80.5


line stmt bran cond sub pod time code
1             package Data::Password::Filter;
2              
3             $Data::Password::Filter::VERSION = '0.18';
4             $Data::Password::Filter::AUTHORITY = 'cpan:MANWAR';
5              
6             =head1 NAME
7              
8             Data::Password::Filter - Interface to the password filter.
9              
10             =head1 VERSION
11              
12             Version 0.18
13              
14             =cut
15              
16 7     7   61440 use 5.006;
  7         53  
17 7     7   4618 use autodie;
  7         114155  
  7         29  
18 7     7   56080 use Data::Dumper;
  7         41719  
  7         500  
19 7     7   2300 use File::Share ':all';
  7         168332  
  7         1059  
20 7     7   3008 use Data::Password::Filter::Params qw(ZeroOrOne FilePath PositiveNum);
  7         24  
  7         67  
21              
22 7     7   6765 use Moo;
  7         67854  
  7         31  
23 7     7   11796 use namespace::autoclean;
  7         76467  
  7         36  
24              
25             =head1 DESCRIPTION
26              
27             The module is a simple attempt to convert an article written by Christopher Frenz
28             on the topic "The Development of a Perl-based Password Complexity Filter".However
29             I took the liberty to add my flavour on top of it.
30              
31             L
32              
33             =cut
34              
35             has [ qw(word_list word_hash) ] => (is => 'ro');
36             has [ qw(check_variation check_dictionary) ] => (is => 'ro', isa => ZeroOrOne, default => sub { return 1; });
37             has [ qw(min_lowercase_letter min_uppercase_letter min_special_character min_digit) ] => (is => 'ro', isa => PositiveNum, default => sub { return 1; });
38             has 'length' => (is => 'ro', isa => PositiveNum, default => sub { return 8; });
39             has 'user_dictionary' => (is => 'ro', isa => FilePath );
40              
41             our $STATUS = {
42             'check_dictionary' => 'Check Dictionary :',
43             'check_length' => 'Check Length :',
44             'check_digit' => 'Check Digit :',
45             'check_lowercase_letter' => 'Check Lowercase Letter :',
46             'check_uppercase_letter' => 'Check Uppercase Letter :',
47             'check_special_character' => 'Check Special Character:',
48             'check_variation' => 'Check Variation :',
49             };
50              
51             sub BUILD {
52 5     5 0 128 my ($self) = @_;
53              
54 5         9 my $dictionary;
55 5 100       32 if ($self->user_dictionary) {
56 1         3 @{$self->{word_list}} = ();
  1         3  
57 1         2 %{$self->{word_hash}} = ();
  1         3  
58 1         3 $dictionary = $self->user_dictionary;
59             }
60             else {
61 4         25 $dictionary = dist_file('Data-Password-Filter', 'dictionary.txt');
62             }
63              
64 5         1052 open(my $DICTIONARY, '<:encoding(UTF-8)', $dictionary);
65 5         46234 while(my $word = <$DICTIONARY>) {
66 393240         503569 chomp($word);
67 393240 100       563662 next if length($word) <= 3;
68 388952         387531 push @{$self->{word_list}}, $word;
  388952         943450  
69             }
70 5         54 close($DICTIONARY);
71              
72             die("ERROR: Couldn't find word longer than 3 characters in the dictionary.\n")
73 5 100       4473 unless scalar(@{$self->{word_list}});
  5         71  
74              
75 4         10 map { $self->{word_hash}->{lc($_)} = 1 } @{$self->{word_list}};
  388952         967377  
  4         2682  
76             }
77              
78             =head1 CONSTRUCTOR
79              
80             Below is the list parameters that can be passed to the constructor. None of the
81             parameters are mandatory. The format of user dictionary should be one word perl
82             line. It only uses the word longer than 3 characters from the user dictionary,
83             if supplied.
84              
85             +-----------------------+---------------------------------------------------+
86             | Key | Description |
87             +-----------------------+---------------------------------------------------+
88             | length | Length of the password. Default is 8. |
89             | | |
90             | min_lowercase_letter | Minimum number of alphabets (a..z) in lowercase. |
91             | | Default is 1. |
92             | | |
93             | min_uppercase_letter | Minimum number of alphabets (A..Z) in uppercase. |
94             | | Default is 1. |
95             | | |
96             | min_special_character | Minimum number of special characters.Default is 1.|
97             | | |
98             | min_digit | Minimum number of digits (0..9). Default is 1. |
99             | | |
100             | check_variation | 1 or 0, depending whether checking variation. |
101             | | Default is 1. |
102             | | |
103             | check_dictionary | 1 or 0, depending whether checking dictionary. |
104             | | Default is 1. |
105             | | |
106             | user_dictionary | User supplied dictionary file location. Default |
107             | | use its own. |
108             +-----------------------+---------------------------------------------------+
109              
110             The C key, when set 1, looks for password that only vary by one
111             character from a dictionary word.
112              
113             =head1 SPECIAL CHARACTERS
114              
115             Currently considers the following characters as special:
116              
117             ! " # $ % & ' ( \ | )
118             ) * + , - . / : ; < =
119             > ? @ [ \ ] ^ _ ` { |
120             } ~
121              
122             =head1 METHODS
123              
124             =head2 strength($password)
125              
126             Returns the strength of the given password and tt is case insensitive.
127              
128             +----------------+------------+
129             | Score (s) | Strength |
130             +----------------+------------+
131             | s <= 50% | Very weak. |
132             | 50% < s <= 70% | Weak. |
133             | 70% < s <= 90% | Good. |
134             | s > 90% | Very good. |
135             +----------------+------------+
136              
137             use strict; use warnings;
138             use Data::Password::Filter;
139              
140             my $password = Data::Password::Filter->new();
141             print "Strength: " . $password->strength('Ab12345?') . "\n";
142              
143             =cut
144              
145             sub strength {
146 3     3 1 988 my ($self, $password) = @_;
147              
148 3 100       32 die("ERROR: Missing password.\n") unless (defined $password);
149              
150 2         11 return $self->_strength($password);
151             }
152              
153             =head2 score($password)
154              
155             Returns the score (percentage) of the given password or the previous password for
156             which the strength has been calculated.
157              
158             use strict; use warnings;
159             use Data::Password::Filter;
160              
161             my $password = Data::Password::Filter->new();
162             print "Score : " . $password->score('Ab12345?') . "\n";
163              
164             $password = Data::Password::Filter->new();
165             print "Strength: " . $password->strength('Ab54321?') . "\n";
166             print "Score : " . $password->score() . "\n";
167              
168             =cut
169              
170             sub score {
171 3     3 1 990 my ($self, $password) = @_;
172              
173 3 100 100     35 die("ERROR: Missing password.\n") unless (defined($password) || defined($self->{score}));
174              
175 2 100       11 $self->_strength($password) if defined $password;
176              
177 2         24 return $self->{score};
178             }
179              
180             =head2 as_string()
181              
182             Returns the filter detail.
183              
184             use strict; use warnings;
185             use Data::Password::Filter;
186              
187             my $password = Data::Password::Filter->new();
188             print "Strength: " . $password->strength('Ab12345?') . "\n";
189             print "Score : " . $password->score('Ab12345?') . "\n";
190             print $password->as_string() . "\n";
191              
192             =cut
193              
194             sub as_string {
195 0     0 1 0 my ($self) = @_;
196              
197 0 0       0 return unless defined $self->{result};
198              
199 0         0 my $string = '';
200 0         0 foreach (keys %{$STATUS}) {
  0         0  
201 0 0 0     0 if (defined($self->{result}->{$_}) && ($self->{result}->{$_})) {
202 0         0 $string .= sprintf("%s %s\n", $STATUS->{$_}, '[PASS]');
203             }
204             else {
205 0         0 $string .= sprintf("%s %s\n", $STATUS->{$_}, '[FAIL]');
206             }
207             }
208              
209 0         0 return $string;
210             }
211              
212             #
213             #
214             # PRIVATE METHODS
215              
216             sub _strength {
217 3     3   12 my ($self, $password) = @_;
218              
219 3 50       29 $self->_checkDictionary($password) if $self->{check_dictionary};
220 3 50       22 $self->_checkVariation($password) if $self->{check_variation};
221 3         34 $self->_checkLength($password);
222 3         15 $self->_checkDigit($password);
223 3         17 $self->_checkUppercaseLetter($password);
224 3         13 $self->_checkLowercaseLetter($password);
225 3         40 $self->_checkSpecialCharacter($password);
226              
227 3         7 my ($count, $score);
228 3         8 $count = 0;
229 3         6 foreach (keys %{$STATUS}) {
  3         58  
230 21 50 33     85 $count++ if (defined($self->{result}->{$_}) && ($self->{result}->{$_}));
231             }
232              
233 3         12 $score = (100/(keys %{$STATUS})) * $count;
  3         24  
234 3         35 $self->{score} = sprintf("%d%s", int($score), '%');
235              
236 3 50 33     52 if ($score <= 50) {
    50 33        
    50          
    50          
237 0         0 return 'Very weak';
238             }
239             elsif (($score > 50) && ($score <= 70)) {
240 0         0 return 'Weak';
241             }
242             elsif (($score > 70) && ($score <= 90)) {
243 0         0 return 'Good';
244             }
245             elsif ($score > 90) {
246 3         25 return 'Very good';
247             }
248             }
249              
250             sub _exists {
251 5     5   13 my ($self, $word) = @_;
252              
253 5         49 return exists($self->{'word_hash'}->{lc($word)});
254             }
255              
256             sub _checkDictionary {
257 5     5   29 my ($self, $password) = @_;
258              
259 5         21 $self->{result}->{'check_dictionary'} = !$self->_exists($password);
260             }
261              
262             sub _checkLength {
263 3     3   20 my ($self, $password) = @_;
264              
265 3         23 $self->{result}->{'check_length'} = !(length($password) < $self->{length});
266             }
267              
268             sub _checkDigit {
269 3     3   11 my ($self, $password) = @_;
270              
271 3         8 my $count = 0;
272 3         43 $count++ while ($password =~ /\d/g);
273              
274 3         17 $self->{result}->{'check_digit'} = !($count < $self->{min_digit});
275             }
276              
277             sub _checkLowercaseLetter {
278 3     3   10 my ($self, $password) = @_;
279              
280 3         8 my $count = 0;
281 3         16 $count++ while ($password =~ /[a-z]/g);
282              
283 3         13 $self->{result}->{'check_lowercase_letter'} = !($count < $self->{min_lowercase_letter});
284             }
285              
286             sub _checkUppercaseLetter {
287 3     3   11 my ($self, $password) = @_;
288              
289 3         7 my $count = 0;
290 3         16 $count++ while ($password =~ /[A-Z]/g);
291              
292 3         16 $self->{result}->{'check_uppercase_letter'} = !($count < $self->{min_uppercase_letter});
293             }
294              
295             sub _checkSpecialCharacter {
296 3     3   8 my ($self, $password) = @_;
297              
298 3         8 my $count = 0;
299 3         31 $count++ while ($password =~ /!|"|#|\$|%|&|'|\(|\)|\*|\+|,|-|\.|\/|:|;|<|=|>|\?|@|\[|\\|]|\^|_|`|\{|\||}|~/g);
300              
301 3         15 $self->{result}->{'check_special_character'} = !($count < $self->{min_special_character});
302             }
303              
304             sub _checkVariation {
305 10     10   37 my ($self, $password) = @_;
306              
307 10 50 33     96 unless (defined($self->{result}->{'check_dictionary'}) && ($self->{result}->{'check_dictionary'})) {
308 0         0 $self->{result}->{'check_variation'} = 0;
309 0         0 return;
310             }
311              
312 10         23 my ($regexp, @_password);
313 10         47 for (my $i = 0; $i <= (length($password)-1); $i++) {
314 87         148 pos($password) = 0;
315 87         251 while ($password =~ /(\w)/gc) {
316 735         1031 my $char = $1;
317 735         794 my $spos = pos($password)-1;
318 735 100       1008 $char = '.' if ($spos == $i);
319 735 100       1699 (defined($_password[$i]))
320             ?
321             ($_password[$i] .= $char)
322             :
323             ($_password[$i] = $char);
324             }
325 87         214 $regexp .= $_password[$i] . '|';
326             }
327 10         89 $regexp =~ s/\|$//g;
328              
329 10         32 foreach (@{$self->{'word_list'}}) {
  10         37  
330 666204 100       1275347 if (/$regexp/i) {
331 4         39 $self->{result}->{'check_variation'} = 0;
332 4         112 return;
333             }
334             }
335              
336 6         200 $self->{result}->{'check_variation'} = 1;
337             }
338              
339             =head1 AUTHOR
340              
341             Mohammad S Anwar, C<< >>
342              
343             =head1 REPOSITORY
344              
345             L
346              
347             =head1 BUGS
348              
349             Please report any bugs or feature requests to C,
350             or through the web interface at L.
351             I will be notified and then you'll automatically be notified of progress on your
352             bug as I make changes.
353              
354             =head1 SUPPORT
355              
356             You can find documentation for this module with the perldoc command.
357              
358             perldoc Data::Password::Filter
359              
360             You can also look for information at:
361              
362             =over 4
363              
364             =item * RT: CPAN's request tracker
365              
366             L
367              
368             =item * AnnoCPAN: Annotated CPAN documentation
369              
370             L
371              
372             =item * CPAN Ratings
373              
374             L
375              
376             =item * Search CPAN
377              
378             L
379              
380             =back
381              
382             =head1 ACKNOWLEDGEMENT
383              
384             Christopher Frenz, author of "Visual Basic and Visual Basic .NET for Scientists
385             and Engineers" (Apress) and "Pro Perl Parsing" (Apress).
386              
387             =head1 LICENSE AND COPYRIGHT
388              
389             Copyright (C) 2011 - 2016 Mohammad S Anwar.
390              
391             This program is free software; you can redistribute it and / or modify it under
392             the terms of the the Artistic License (2.0). You may obtain a copy of the full
393             license at:
394              
395             L
396              
397             Any use, modification, and distribution of the Standard or Modified Versions is
398             governed by this Artistic License.By using, modifying or distributing the Package,
399             you accept this license. Do not use, modify, or distribute the Package, if you do
400             not accept this license.
401              
402             If your Modified Version has been derived from a Modified Version made by someone
403             other than you,you are nevertheless required to ensure that your Modified Version
404             complies with the requirements of this license.
405              
406             This license does not grant you the right to use any trademark, service mark,
407             tradename, or logo of the Copyright Holder.
408              
409             This license includes the non-exclusive, worldwide, free-of-charge patent license
410             to make, have made, use, offer to sell, sell, import and otherwise transfer the
411             Package with respect to any patent claims licensable by the Copyright Holder that
412             are necessarily infringed by the Package. If you institute patent litigation
413             (including a cross-claim or counterclaim) against any party alleging that the
414             Package constitutes direct or contributory patent infringement,then this Artistic
415             License to you shall terminate on the date that such litigation is filed.
416              
417             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND
418             CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED
419             WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
420             NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS
421             REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT,
422             INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE
423             OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
424              
425             =cut
426              
427             1; # End of Data::Password::Filter