File Coverage

blib/lib/Crypt/YAPassGen.pm
Criterion Covered Total %
statement 121 172 70.3
branch 19 50 38.0
condition 11 23 47.8
subroutine 22 26 84.6
pod 11 11 100.0
total 184 282 65.2


line stmt bran cond sub pod time code
1             package Crypt::YAPassGen;
2              
3             $Crypt::YAPassGen::VERSION = 0.02;
4              
5 1     1   2820 use 5.006;
  1         6  
  1         54  
6 1     1   5 use strict;
  1         2  
  1         302  
7 1     1   4730 use locale;
  1         723  
  1         7  
8 1     1   37 use Carp;
  1         2  
  1         431  
9 1     1   986562 use Storable qw(nstore retrieve);
  1         5002  
  1         101  
10 1     1   12 use File::Spec;
  1         2  
  1         27  
11 1     1   6 use Config;
  1         2  
  1         43  
12 1     1   5 use base 'Class::Data::Inheritable';
  1         2  
  1         1107  
13              
14             __PACKAGE__->mk_classdata('DEFAULT_DICT');
15             __PACKAGE__->DEFAULT_DICT( File::Spec->catfile(
16             $Config{installsitelib}, qw(Crypt YAPassGen american-english.dat)
17             ) );
18              
19             __PACKAGE__->mk_classdata('PRECOOKED_SUBS');
20             __PACKAGE__->PRECOOKED_SUBS( {
21             haxor => sub { tr/aAeEtTsSoOiIzZgG/4433775500112266/ },
22             digits => sub { s/(.)/ rand() < 0.25 ? int(rand 10) . $1 : $1 /ge },
23             caps => sub {
24             my $rand = rand;
25             s/(.)/ rand() < $rand ? uc $1 : $1 /ge;
26             },
27             } );
28              
29              
30             __PACKAGE__->mk_classdata('ALGORITHMS');
31             __PACKAGE__->ALGORITHMS( {
32             linear => sub { _weighted_rand( _weight_to_dist( &_getref ) ) },
33             sqrt => sub { _weighted_rand( _weight_to_dist_sqrt( &_getref ) ) },
34             log => sub { _weighted_rand( _weight_to_dist_log( &_getref ) ) },
35             flat => sub { _flat_rand( &_getref ) },
36             } );
37              
38              
39             sub new {
40 2     2 1 4262 my $class = shift;
41            
42 2         1584 my %h = (
43             length => 8,
44             freq => __PACKAGE__->DEFAULT_DICT,
45             post_subs => undef,
46             algorithm => 'sqrt',
47             ascii => 0,
48             @_
49             );
50              
51 2         52 my $self = bless {
52             post_subs => [],
53             freq => {},
54             }, $class;
55            
56 2         16 $self->$_( $h{$_} ) for qw(length freq post_subs algorithm ascii);
57            
58 2         14 return $self;
59             }
60              
61             sub algorithm {
62 3     3 1 1221 my $self = shift;
63 3         5 my ($alg) = @_;
64 3 50       11 if (defined $alg) {
65 3 50       11 if (ref($alg) eq 'CODE') {
66 0         0 $self->{algorithm} = $alg;
67             } else {
68 3 50       24 croak qq(No such algorithm "$alg")
69             unless __PACKAGE__->ALGORITHMS->{$alg};
70 3         38 $self->{algorithm} = __PACKAGE__->ALGORITHMS->{$alg};
71             }
72             }
73 3         32 return $self->{algorithm};
74             }
75              
76             sub ascii {
77 2     2 1 4 my $self = shift;
78 2         5 my ($ascii) = @_;
79 2 50       12 $self->{ascii} = $ascii if defined $ascii;
80 2         8 return $self->{ascii};
81             }
82              
83             sub freq {
84 2     2 1 6 my $self = shift;
85 2         4 my ($freq) = @_;
86 2 50       8 if (defined $freq) {
87 2 50 33     136 if ($freq eq '') {
    50          
88 0         0 $self->{freq} = {};
89 0         0 $self->{freq_file} = '';
90             } elsif (-e $freq and -r _) {
91 2         17 $self->{freq} = retrieve( $freq );
92 2         26598 $self->{freq_file} = $freq;
93             } else {
94 0         0 croak qq(Cannot find/read the frequency file "$freq");
95             }
96             }
97 2         25 return $self->{freq_file};
98             }
99              
100             sub post_subs {
101 2     2 1 6 my $self = shift;
102 2         5 my ($sub) = @_;
103            
104 2 50       10 if (defined $sub) {
105 0 0       0 if ( ref($sub) eq 'ARRAY' ) {
106 0         0 $self->reset_post_subs;
107 0         0 $self->add_post_sub( $_ ) for @$sub;
108             } else {
109 0         0 croak "Not an ARRAY reference";
110             }
111             }
112            
113 2         14 return $self->{post_subs};
114             }
115              
116             sub add_post_sub {
117 1     1 1 663 my $self = shift;
118 1         2 my ($sub) = @_;
119 1 50       5 if (defined $sub) {
120 1 50       5 if ( ref($sub) eq 'CODE' ) {
    0          
121 1         1 push @{ $self->{post_subs} }, $sub;
  1         4  
122             } elsif ( __PACKAGE__->PRECOOKED_SUBS->{$sub} ) {
123 0         0 push @{ $self->{post_subs} }, __PACKAGE__->PRECOOKED_SUBS->{$sub};
  0         0  
124             } else {
125 0         0 carp qq(No such precooked sub "$sub");
126 0         0 return;
127             }
128             }
129 1         3 return $self->{post_subs};
130             }
131              
132             sub reset_post_subs {
133 2     2 1 1459 my $self = shift;
134 2         6 my $old_subs = $self->{post_subs};
135 2         6 $self->{post_subs} = [];
136 2         7 return $old_subs;
137             }
138              
139             sub length {
140 4     4 1 2249 my $self = shift;
141 4         6 my ($length) = @_;
142 4 100       16 if (defined $length) {
143 3 50       9 croak "Length must be an integer >= 1" unless $length >= 1;
144 3         18 $self->{length} = $length;
145             }
146 4         539 return $self->{length};
147             }
148              
149             sub generate {
150 6     6 1 2979 my $self = shift;
151 6         9 my (@passwd, $passwd);
152 6         20 while ( @passwd < $self->{length} ) {
153 61         157 push @passwd, $self->{algorithm}->( $self, \@passwd );
154             }
155 6         20 $passwd = join('', @passwd);
156 6 50       19 _striphigh( $passwd ) if $self->{ascii};
157 6         11 for ($passwd) {
158 6         11 for my $sub ( @{ $self->{post_subs} } ) {
  6         26  
159 1         6 $sub->();
160             }
161             }
162            
163             #_striphigh or post_sub may lengthen $passwd, so we truncate it
164 6         26 substr($passwd, $self->{length}) = '';
165            
166 6         27 return $passwd;
167             }
168              
169             sub make_freq {
170 0     0 1 0 my $proto = shift;
171 0         0 my ($input, $output, $ascii) = @_;
172 0 0       0 my @files = ref($input) ? @$input : ($input);
173            
174 0         0 my (%prob, $self);
175              
176 0 0       0 if (ref $proto) {
177 0         0 $self = $proto;
178 0         0 %prob = %{ $self->{freq} };
  0         0  
179 0         0 $ascii = $self->{ascii};
180             }
181              
182 0         0 for my $file (@files) {
183 0         0 local *IN;
184 0 0       0 open(IN, "<", $file) or croak qq(Cannot open dict file "$file" : $!);
185 0         0 while () {
186 0         0 while (/([[:alpha:]]{3,})/g) {
187 0         0 my $word = lc $1;
188 0 0       0 _striphigh( $word ) if $ascii;
189 0         0 my @word = split //, $word;
190 0         0 for (my ($i,$j) = (0,2); $j < @word; $i++,$j++) {
191 0         0 my $ref = \%prob;
192 0         0 for (@word[$i..$j]) {
193 0         0 $ref->{ $_ }[0]++;
194 0   0     0 $ref->{ $_ }[1] ||= {};
195 0         0 $ref = $ref->{ $_ }[1];
196             }
197             }
198             }
199             }
200 0         0 close(IN);
201             }
202            
203 0 0       0 nstore(\%prob, $output) if defined $output;
204              
205 0 0       0 $self->{freq} = \%prob if $self;
206            
207 0         0 return \%prob;
208             }
209              
210             sub save_freq {
211 0     0 1 0 my $self = shift;
212 0         0 my ($file) = @_;
213 0         0 nstore($self->{freq}, $file);
214             }
215              
216             ### UTILITIES
217              
218             sub _getref {
219 61     61   113 my ($self, $passwd) = @_;
220 61         103 my ($f, $s) = @$passwd[-2, -1];
221 61 50 66     274 if ($f and $self->{freq}{ $f } #ton of stuff
  49   66     451  
      33        
      33        
222 49         286 and %{ $self->{freq}{ $f }[1] } #to prevent
223             and $self->{freq}{ $f }[1]{ $s } #autovivification
224             and %{ $self->{freq}{ $f }[1]{ $s }[1] }) {
225 49         161 return $self->{freq}{ $f }[1]{ $s }[1];
226             }
227 12 50 66     64 if ($s and $self->{freq}{ $s }
  6   66     53  
228             and %{ $self->{freq}{ $s }[1] }) {
229 6         23 return $self->{freq}{ $s }[1];
230             }
231 6         23 return $self->{freq};
232             }
233              
234             sub _weight_to_dist {
235 10     10   17 my ($weights) = @_;
236 10         16 my %dist = ();
237 10         13 my $total = 0;
238 10         11 my ($key, $weight);
239              
240 10         128 $total += $_->[0] for values %$weights;
241            
242 10         35 while ( ($key, $weight) = each %$weights ) {
243 193         561 $dist{$key} = $weight->[0] / $total;
244             }
245            
246 10         25 return \%dist;
247             }
248              
249             sub _weight_to_dist_sqrt {
250 8     8   8 my ($weights) = @_;
251 8         10 my (%dist, %temp) = ();
252 8         11 my $total = 0;
253 8         8 my ($key, $weight);
254              
255 8         208 $total += $temp{$_} = sqrt $weights->{$_}[0] for keys %$weights;
256            
257 8         32 while ( ($key, $weight) = each %temp ) {
258 167         351 $dist{$key} = $weight / $total;
259             }
260            
261 8         34 return \%dist;
262             }
263              
264             sub _weight_to_dist_log {
265 43     43   55 my ($weights) = @_;
266 43         74 my (%dist, %temp) = ();
267 43         48 my $total = 0;
268 43         48 my ($key, $weight);
269              
270 43         1286 $total += $temp{$_} = log $weights->{$_}[0] for keys %$weights;
271            
272 43 50       154 return _weight_to_dist( $weights ) if $total == 0;
273            
274 43         127 while ( ($key, $weight) = each %temp ) {
275 778         2119 $dist{$key} = $weight / $total;
276             }
277            
278 43         201 return \%dist;
279             }
280              
281             sub _weighted_rand {
282 61     61   78 my ($dist) = @_;
283 61         61 my ($key, $weight);
284              
285 61         61 while (1) { # to avoid floating point inaccuracies
286 61         140 my $rand = rand;
287 61         215 while ( ($key, $weight) = each %$dist ) {
288 574 100       2124 return $key if ($rand -= $weight) < 0;
289             }
290             }
291             }
292              
293             sub _flat_rand {
294 0     0     my ($weights) = @_;
295 0           my @chars = grep {$weights->{$_}[0] > 0} keys %$weights;
  0            
296 0           return $chars[int rand @chars];
297             }
298              
299             sub _striphigh {
300 0     0     $_[0] =~ tr{àáâãäåªèéêëìíîïòóôõöøºùúûüýÿçñþð}
301             {aaaaaaaeeeeiiiiooooooouuuuyycntd};
302 0           $_[0] =~ s/½/oe/g; $_[0] =~ s/æ/ae/g;
  0            
303 0           $_[0] =~ s/ß/ss/g; $_[0] =~ s/µ/mu/g;
  0            
304             }
305              
306             =head1 NAME
307              
308             Crypt::YAPassGen - Yet Another (pronounceable) Password Generator
309              
310             =head1 SYNOPSIS
311              
312             use Crypt::YAPassGen;
313              
314             my $passgen = Crypt::YAPassGen->new(
315             freq => '/usr/share/dict/mobydick.dat',
316             length => 10,
317             post_subs => [sub { $_ = uc }, "digits"],
318             );
319              
320             my $passwd = $passgen->generate();
321              
322             =head1 DESCRIPTION
323              
324             C allows you to generate pronounceable passwords using a
325             frequency file extracted from a dictionary of words.
326             This module was inspired by C written by Tim Jenness. I started
327             writing this module a couple of
328             years ago, because I wasn't able to make C work with an Italian
329             frequency file.
330             This module also offers a different interface and a few more options than
331             Crypt::PassGen, that's why it exists. See L for other similar
332             modules.
333             Please beware that passwords generated by this module are LESS secure than
334             truly random passwords, so use it at your own risk!
335              
336             =head1 USAGE
337              
338             =head2 CLASS METHODS
339              
340             =over 4
341              
342             =item my $passgen = Crypt::YAPassGen->new(%opts)
343              
344             Returns a new password generator object. You can pass an hash of options: every
345             option will be treated as a call to the object method of the same name.
346             Allowed options are C, C, C, C and C.
347             If an option is not specified the newly generated object will use the
348             following defaults:
349              
350             freq => '/path_to_american-english_default_freq_file.dat',
351             length => 8,
352             algorithm => 'sqrt',
353             ascii => 0,
354             post_subs => [], #NONE
355              
356             =item my $freq = Crypt::YAPassGen->make_freq($dict_file, $freq_file, $ascii)
357              
358             This class method will generate a new frequency file reading from C<$dict_file>
359             and writing the result in C<$freq_file>. If C<$dict_file> is an ARRAY reference,
360             then we consider the elements of the array as filenames and we process all of
361             them.
362             The C<$ascii> flag is optional. This is useful if your locale allows for
363             alphabetic characters out of the 7 bit Latin ASCII alphabet (for example
364             accented characters or with umlaut). It is higly suggested to set this variable
365             to a true value unless your locale is US-ASCII or you're sure your dictionary
366             doesn't contain any accented character. This apporach works fine for most
367             european locales, but I'm not sure what would happen with different locales.
368              
369             =back
370              
371             =head2 OBJECT METHODS
372              
373             =over 4
374              
375             =item my $passwd = $passgen->generate()
376              
377             Generate a password with previously defined options.
378              
379             =item my $length = $passgen->length($integer)
380              
381             Get/set the desired length for generated passwords.
382              
383             =item my $freq_file = $passgen->freq($filename)
384              
385             Get/set the frequency file to use.
386             If set to an empty string it will clear the internal frequency table and
387             you will have to call C on the object before trying to
388             C any new password.
389              
390             =item my $ascii = $passgen->ascii($flag);
391              
392             Get/set the ascii flag. If it's true then we are sure our passwords will be made
393             only of 7 bit ASCII characters as long as frequency file contains only 7 bit
394             ASCII alphabet characters and accented variants of the same.
395              
396             =item my $algorithm = $passgen->algorithm($code_or_string)
397              
398             Get/set the algorithm to calculate the sequence of letters to be addedd to the
399             password. The returned value will be a CODE reference. The method accept as
400             parameters either a CODE reference or a string. If it's a string it can be one
401             of the following: "linear", "sqrt", "log" and "flat".
402              
403             The "linear" algorithm calculate the sequence of characters with a function
404             linear to the frequency of the characters. This generate really pronounceable
405             passwords, but may be too easy to crack.
406              
407             The "sqrt" algorithm is the default as the password are still pronounceable but
408             a bit harder to crack.
409              
410             The "log" algorithm is similar to the "sqrt" but not as consistent.
411              
412             The "flat" algorithm is really fast, but the generated passwords look more like
413             really random strings than pronounceable words.
414              
415             If you are interested in personalizing the algorithm used you should take a
416             look at the code, brew your own algorithm and then pass it in as a CODE
417             reference.
418              
419             =item my $post_subs = $passgen->add_post_sub($code_ref)
420              
421             Adds a sub to the stack of procedures that will be executed once the password
422             has been produced. The subs are supposed to modify C<$_> as in a C
423             loop.
424             Here's an example to have all upper-case passwords:
425              
426             $passgen->add_post_sub(sub { tr/a-z/A-Z/ });
427              
428             Please note that if the sub lengthen the password, then it will be later
429             truncated
430             at the right length, but if it shorten the password then you will be left with
431             a mutilated one.
432              
433             Instead of passing a code reference you may pass a string corresponding to one
434             of the pre-cooked subs available in this module. They are the following:
435              
436             "haxor": change some of the characters into l33t version of the same
437              
438             "caps": insert a random amount of upper-case characters
439              
440             "digits": insert some digits with a 1 in 4 probability
441              
442             =item my $post_subs = $passgen->post_subs([@code_refs])
443              
444             Get/set the code refs to the subs that will be called after the production of
445             the password. See C for specification of the subs.
446             Returns a reference to the ARRAY of subs to be processed.
447             Example:
448              
449             $passgen->post_subs([sub { tr/t/+/ }, "caps", "haxor"]);
450              
451             =item my $old_subs = $passgen->reset_post_subs()
452              
453             Reset the ARRAY of subs.
454             Returns a reference to the ARRAY of subs that were there.
455              
456             =item my $freq = $passgen->make_freq($dict_file, $freq_file)
457              
458             This class method will generate a new frequency file reading from C<$dict_file>
459             and writing the result in C<$freq_file>. If C<$dict_file> is an ARRAY reference
460             then we consider the elements of the array as filenames and we process all of
461             them.
462             You may omit C<$freq_file> in which case the result won't be saved to disk, but
463             it will still be contained by C<$passgen> so that you may use it on the fly.
464             If you call this method when a frequency table is already loaded in the object,
465             the new frequency will be just added to the one already present in the object
466             so that you can mix different dictionaries.
467              
468             =item $passgen->save_freq($filename)
469              
470             Save the frequency table contained in the object to C<$filename>.
471              
472             =back
473              
474             =head1 TODO
475              
476             -adding more post_subs?
477              
478             -bit more l10n effort?
479              
480             =head1 BUGS
481              
482             Not really a bug in itself but this module is NOT secure! Use it at your own
483             risk!
484              
485             =head1 SEE ALSO
486              
487             This module was originally inspired by C by Tim Jenness so you
488             may notice some similarities.
489             Modules similar to this one include C,
490             C, C and C.
491              
492             =head1 COPYRIGHT
493              
494             Copyright 2002-2004 Giulio Motta L.
495              
496             This library is free software; you can redistribute it and/or
497             modify it under the same terms as Perl itself.
498              
499             =cut
500              
501             1;