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