File Coverage

blib/lib/Data/Password/Manager.pm
Criterion Covered Total %
statement 99 100 99.0
branch 55 60 91.6
condition 3 5 60.0
subroutine 11 11 100.0
pod 5 9 55.5
total 173 185 93.5


line stmt bran cond sub pod time code
1             package Data::Password::Manager;
2              
3 2     2   1333 use strict;
  2         3  
  2         400  
4             #use diagnostics;
5             #use warnings;
6 2     2   12 use vars qw($VERSION @ISA @EXPORT_OK @to64);
  2         2  
  2         2794  
7             require Exporter;
8             @ISA = qw(Exporter);
9             $VERSION = do { my @r = (q$Revision: 0.05 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r };
10              
11             @EXPORT_OK = qw(
12             pw_gen
13             pw_valid
14             pw_clean
15             pw_obscure
16             pw_get
17             );
18              
19             #
20             # crypto lib to decode and encode passwords
21             #
22              
23             =head1 NAME
24              
25             Data::Password::Manager - generate, check, manage B passwords
26              
27             =head1 SYNOPSIS
28              
29             use Data::Password::Manager qw(
30             pw_gen
31             pw_valid
32             pw_obscure
33             pw_clean
34             pw_get
35             );
36              
37             $password = pw_gen($cleartext);
38             $ok = pw_valid($cleartxt,$password);
39             $clean_text = pw_clean($dirty_text);
40             ($code,$text) = $pw_obscure($newpass,$oldpass,$min_len);
41             $passwd = pw_get($user,$passwd_file,\$error);
42              
43             =head1 DESCRIPTION
44              
45             =over 2
46              
47             =item * $password = pw_gen($cleartext);
48              
49             Generate a 13 character DES password string from clear text
50              
51             input: string <= 128 characters
52             output: password
53              
54             =cut
55              
56             ###############################################
57             # Subroutine to encrypt a string
58             # uses 'crypt'
59              
60             sub pw_gen {
61 4     4 1 117 my($clrtxt) = @_;
62 4         14 my $seed = time + unpack("%16C*", $clrtxt);
63 4         92 srand; # init rand generator
64 4         9 my $salt = &salt_char($clrtxt); # first salt char
65 4         9 srand $seed; # alter rand generator
66 4         8 $salt .= &salt_char($clrtxt); # 2nd salt char
67 4 50       11 $clrtxt = substr($clrtxt,0,128) if length $clrtxt > 128;
68 4         116 return crypt($clrtxt,$salt); # password
69             }
70              
71             =item * $ok = pw_valid($cleartxt,$password);
72              
73             Return true if clear text is password match
74              
75             input: string <= 128 characters,
76             password
77             output: true on match, else false
78              
79             =cut
80              
81             ###############################################
82             # Subroutine to check a password
83             # uses 'crypt'
84             #
85              
86             sub pw_valid {
87 5     5 1 36 my($plaintxt, $passwd) = @_;
88 5 50       627 return crypt($plaintxt, $passwd) eq $passwd
89             if $passwd;
90             # for blank encrypted passwords
91 0         0 return $plaintxt eq $passwd;
92             }
93              
94             =item * $clean_text = pw_clean($dirty_text);
95              
96             Clean a text string to only include
97             / . 0..9 a..z A..Z
98              
99             Useful for restricted password sets
100             i.e. http applications
101              
102             Returns a string of 128 characters or less
103              
104             =cut
105              
106             ###############################################
107             # Subroutine to clean string for crypt use
108             #
109             # Input: [string]
110             # Output: [clean string <128 characters]
111              
112             sub pw_clean {
113 1     1 1 47 my $string = $_[0];
114 1         5 $string =~ tr/[a-zA-Z0-9\/\.]//cd;
115 1 50       8 $string = (length $string > 128)
116             ? substr($string,0,128)
117             : $string;
118             }
119              
120             =item * ($code,$text) = $pw_obscure($newpass,$oldpass);
121              
122             Check for a usable password. Returns ok if there is
123             no old password. i.e. any new password will do.
124              
125             input: string <= 128 characters
126              
127             return (0, 'OK') if no old password or new is good
128             return ( 1, 'too short' ) if length < $MIN_LEN (default 5)
129             return ( 2, 'no change' ) if old eq new
130             return ( 3, 'a palindrome' ) if new is a palindrome
131             return ( 4, 'case change only' ) if old =~ /$new$/i
132             return ( 5, 'to similar' ) see code
133             return ( 6, 'to simple' ) if not a good character mix
134             return ( 7, 'rotated' ) if new is rotated version of old
135             return ( 8, 'flipped' ) if new is old flipped around
136              
137             =cut
138              
139             ### sub-subroutine to pick a character not in the input string
140             # Input: [string]
141             # Output: [character]
142             #
143              
144             @to64 = ('.', '/', 0..9,'A'..'Z', 'a'..'z');
145              
146             sub salt_char {
147 8     8 0 8 my($clrtxt) = @_;
148 8         16 my $salt = $to64[rand 64];
149             # try once again if the character is in the clear text string
150 8 100       64 $salt = ($clrtxt =~ /$salt/)
151             ? $to64[rand 64]
152             : $salt;
153             }
154              
155             =item * $passwd=pw_get($user,$passwd_file,\$error);
156              
157             Check a password file for the presence of $user:$password.
158              
159             input: $user
160             return: $password
161              
162             Returns undef on error and places a descriptive error message in the scalar
163             $error. Since a valid password can be empty, the caller must check that the
164             return value is defined, not just false.
165              
166             FILE is of the form:
167              
168             user1:DESpassword1
169             user2:DESpassword2
170             etc...
171             # in-line comments are OK
172              
173             =back
174              
175             =cut
176              
177             # return encrypted password or undef
178             # since the password can be empty,
179             # caller must check if "defined"
180              
181             sub pw_get {
182 2     2 1 684 my($user,$passwd_file,$ep) = @_;
183 2         3 $$ep = 'could not access password file';
184 2 50       54 open(P,$passwd_file)
185             or return undef;
186 2         6 $$ep = "no such user, $user";
187 2         4 my $passwd;
188 2         41 foreach(

) {

189 7 100       97 if ($_ =~ /^${user}:(.*)/) {
190 1         2 $$ep = '';
191 1         4 $passwd = $1;
192 1         2 last;
193             }
194             }
195 2         17 close P;
196             return undef
197 2 100       10 if $$ep;
198 1   50     6 return $passwd || '';
199             }
200              
201              
202             #######################################################
203             # subroutine to check for obscure password
204             # This is a port to perl of the functions found
205             # in the shadow suite.
206             #######################################################
207             #
208             # Copyright 1989 - 1994, Julianne Frances Haugh
209             # All rights reserved.
210             #
211             # Redistribution and use in source and binary forms, with or without
212             # modification, are permitted provided that the following conditions
213             # are met:
214             # 1. Redistributions of source code must retain the above copyright
215             # notice, this list of conditions and the following disclaimer.
216             # 2. Redistributions in binary form must reproduce the above copyright
217             # notice, this list of conditions and the following disclaimer in the
218             # documentation and/or other materials provided with the distribution.
219             # 3. Neither the name of Julianne F. Haugh nor the names of its contributors
220             # may be used to endorse or promote products derived from this software
221             # without specific prior written permission.
222             #
223             # THIS SOFTWARE IS PROVIDED BY JULIE HAUGH AND CONTRIBUTORS `AS IS'' AND
224             # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
225             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
226             # ARE DISCLAIMED. IN NO EVENT SHALL JULIE HAUGH OR CONTRIBUTORS BE LIABLE
227             # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
228             # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
229             # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
230             # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
231             # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
232             # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
233             # SUCH DAMAGE.
234              
235             # RCSID("$Id: obscure.c,v 1.7 1998/04/16 19:57:44 marekm Exp $")
236             #
237             # This version of obscure.c contains modifications to support "cracklib"
238             # by Alec Muffet (alec.muffett@uk.sun.com). You must obtain the Cracklib
239             # library source code for this function to operate.
240             #
241             #######################################################
242             # subroutine to do the password checking
243             #
244             # Input: $newpass,[$oldpass],[$minpasslen]
245             # Return: ( code, message )
246             #
247             # 0 OK
248             # !0 something wrong
249             #
250             #sub CryptLibObscure {
251             # my($old,$new) = @_;
252             # @_ = ($new,$old);
253             # goto &pw_obscure;
254             #}
255              
256             sub pw_obscure {
257 23     23 1 293 my ( $oldmono, $newmono );
258              
259             # my ( $old, $new ) = @_;
260 23         32 my ($new,$old,$min_pass_len) = @_;
261 23 50       40 $min_pass_len = 5 unless $min_pass_len;
262              
263 23 100       40 return (9,'missing new password') unless $new;
264 22 100       44 return (0,'OK') unless $old;
265              
266            
267 21         25 ( $oldmono = $old ) =~ tr/A-Z/a-z/;
268 21         23 ( $newmono = $new ) =~ tr/A-Z/a-z/;
269              
270             # @_ = split ( '', $oldmono ); # turn $old end for end
271             # while ( $i = pop ) {
272             # $flipped .= $i;
273             # }
274             # $flipped .= $flipped;
275              
276 21         33 my $flipped = reverse $oldmono;
277              
278 21 100       36 return ( 1, 'too short' ) if ( length ($new) < $min_pass_len );
279 20 100       31 return ( 2, 'no change' ) if ( $old eq $new );
280 19 100       28 return ( 3, 'a palindrome' ) if ( &CL_Palindrome ($oldmono, $newmono));
281 18 100       36 return ( 4, 'case change only' ) if ( $oldmono eq $newmono );
282 17 100       23 return ( 5, 'to similar' ) if ( &CL_Similar ($oldmono, $newmono));
283 16 100       26 return ( 6, 'to simple' ) if ( &CL_Simple ( $old, $new ));
284 3         4 $oldmono .= $oldmono;
285 3 100       27 return ( 7, 'rotated' ) if ( $oldmono =~ /$newmono/ );
286 2 100       19 return ( 8, 'flipped' ) if ( $flipped =~ /$newmono/ );
287 1         3 return (0,'OK');
288             }
289              
290             #######################################################
291             # subroutine to check for palindrome
292             #
293             # Input: [old], [new] passwords
294             # Return: 0, OK
295             # 1, no good
296             #
297             sub CL_Palindrome {
298 19     19 0 16 my ( $i, $j );
299 19         21 my ( $old, $new ) = @_;
300 19         74 my @new = split ( '', $new );
301              
302             # can't be a palindrome - like `R A D A R' or `M A D A M'
303              
304 19         28 $i = @new;
305 19         39 for ( $j=0; $j < $i; $j++ ) {
306 28 100       66 if ( $new[$i-$j-1] ne $new[$j] ) {
307 18         49 return (0);
308             }
309             }
310 1         6 1;
311             }
312              
313             #######################################################
314             # subroutine to check for similarity between new, old password
315             #
316             # Input: [old], [new] passwords
317             # Return: 0, OK
318             # 1, no good
319             #
320             sub CL_Similar {
321 17     17 0 16 my ( $i, $j );
322 17         22 my ( $old, $new ) = @_;
323 17         54 my @old = split ( '', $old );
324              
325             # @new is only used to get cheap length and
326             # a $#new count so the equation below is consistent
327 17         48 my @new = split ( '', $new );
328              
329             # XXX - sometimes this fails when changing from a simple password
330             # to a really long one (MD5). For now, I just return success if
331             # the new password is long enough. Please feel free to suggest
332             # something better... --marekm
333              
334 17 100       31 if ( @new >= 8 ) { return (0); }
  2         7  
335 15         17 $j=0;
336 15   66     57 for ( $i=0; ($i<=$#new && $i<=$#old); $i++ ) {
337             # next line really is $new
338 90 100       737 if ( $new =~ /$old[$i]/ ) { ++$j; }
  26         99  
339             }
340 15 100       31 if ( $i >= $j*2 ) { return (0); }
  14         73  
341 1         7 1;
342             }
343              
344             #######################################################
345             # subroutine to check for a nice mix of characters
346             #
347             # Input: [old], [new] passwords
348             # Return: 0, OK
349             # 1, no good
350             #
351             sub CL_Simple {
352 16     16 0 13 my $i;
353 16         22 my $digits = 0;
354 16         14 my $uppers = 0;
355 16         16 my $lowers = 0;
356 16         12 my $others = 0;
357            
358 16         21 my ( $old, $new ) = @_;
359 16         68 my @new = split ( '', $new );
360              
361 16         39 for ( $i=0; $i <= $#new; $i++ ) {
362 107 100       305 if ( $new[$i] =~ /[0-9]/ ) { ++$digits;
  20 100       32  
    100          
363 47         80 } elsif ( $new[$i] =~ /[a-z]/ ) { ++$lowers;
364 35         68 } elsif ( $new[$i] =~ /[A-Z]/ ) { ++$uppers;
365 5         9 } else { ++$others; }
366             }
367              
368             # The scam is this - a password of only one character type
369             # must be 8 letters long. Two types, 7, and so on.
370              
371 16         18 my $size = 9;
372 16 100       25 if ( $digits ) { --$size; }
  7         7  
373 16 100       34 if ( $uppers ) { --$size; }
  11         10  
374 16 100       30 if ( $lowers ) { --$size; }
  10         9  
375 16 100       19 if ( $others ) { --$size; }
  5         5  
376              
377 16 100       29 if ( $size <= $i ) { return (0); }
  3         10  
378 13         72 1;
379             }
380             1;
381             __END__