File Coverage

blib/lib/Crypt/GeneratePassword.pm
Criterion Covered Total %
statement 101 176 57.3
branch 29 72 40.2
condition 12 28 42.8
subroutine 11 15 73.3
pod 7 9 77.7
total 160 300 53.3


line stmt bran cond sub pod time code
1             package Crypt::GeneratePassword;
2             $Crypt::GeneratePassword::VERSION = '0.04';
3             # ABSTRACT: generate secure random pronounceable passwords
4              
5 4     4   86556 use 5.006;
  4         16  
  4         529  
6 4     4   26 use strict;
  4         9  
  4         153  
7 4     4   31 use warnings;
  4         8  
  4         885  
8              
9             =encoding utf-8
10              
11             =head1 NAME
12              
13             Crypt::GeneratePassword - generate secure random pronounceable passwords
14              
15             =head1 SYNOPSIS
16              
17             use Crypt::GeneratePassword qw(word chars);
18             $word = word($minlen,$maxlen);
19             $word = chars($minlen,$maxlen);
20             *Crypt::GeneratePassword::restrict = \&my_restriction_filter;
21             *Crypt::GeneratePassword::random_number = \&my_random_number_generator;
22              
23             =head1 DESCRIPTION
24              
25             Crypt::GeneratePassword generates random passwords that are
26             (more or less) pronounceable. Unlike Crypt::RandPasswd, it
27             doesn't use the FIPS-181 NIST standard, which is proven to be
28             insecure. It does use a similar interface, so it should be a
29             drop-in replacement in most cases.
30              
31             If you want to use passwords from a different language than english,
32             you can use one of the packaged alternate unit tables or generate
33             your own. See below for details.
34              
35             For details on why FIPS-181 is insecure and why the solution
36             used in this module is reasonably secure, see "A New Attack on
37             Random Pronounceable Password Generators" by Ravi Ganesan and
38             Chris Davies, available online in may places - use your
39             favourite search engine.
40              
41             This module improves on FIPS-181 using a true random selection with
42             the word generator as mere filter. Other improvements are
43             better pronounceability using third order approximation instead
44             of second order and multi-language support.
45             Drawback of this method is that it is usually slower. Then again,
46             computer speed has improved a little since 1977.
47              
48             =head1 Functions
49              
50             =cut
51              
52             require Exporter;
53             our @ISA = ('Exporter');
54             our @EXPORT_OK = qw(word word3 analyze analyze3 chars generate_language load_language);
55             our %EXPORT_TAGS = ( 'all' => [ @Crypt::GeneratePassword::EXPORT_OK ] );
56              
57             my $default_language = 'en';
58 4     4   22 use vars qw(%languages);
  4         10  
  4         2466  
59             %languages = ();
60              
61             =head2 chars
62              
63             $word = chars($minlen, $maxlen [, $set [, $characters, $maxcount ] ... ] );
64              
65             Generatess a completely random word between $minlen and $maxlen in length.
66             If $set is given, it must be an array ref of characters to use. You can
67             restrict occurrence of some characters by providing ($characters, $maxcount)
68             pairs, as many as you like. $characters must be a string consisting of those
69             characters which may appear at most $maxcount times in the word.
70              
71             Note that the length is determined via relative probability, not uniformly.
72              
73             =cut
74              
75             my @signs = ('0'..'9', '%', '$', '_', '-', '+', '*', '&', '/', '=', '!', '#');
76             my $signs = join('',@signs);
77             my @caps = ('A' .. 'Z');
78             my $caps = join('',@caps);
79              
80             my @set = (
81             [ ["\x00",'a'..'z'], ["\x00",'a'..'z',@caps] ],
82             [ ["\x00",'a'..'z',@signs], ["\x00",'a'..'z',@caps,@signs] ]
83             );
84              
85             sub chars($$;$@) {
86 505017     505017 1 854850 my ($minlen, $maxlen, $set, @restrict) = @_;
87 505017   66     1002142 $set ||= $set[1][1];
88 505017         507496 my $res;
89 505017         656742 my $diff = $maxlen-$minlen;
90 11275872         19325091 WORD: {
91 505017         530542 $res = join '', map { $$set[random_number(scalar(@$set))] } 1..$maxlen;
  816502         1852403  
92 816502         5388848 $res =~ s/\x00{0,$diff}$//;
93 816502 100       2305066 redo if $res =~ m/\x00/;
94 505017         1572030 for (my $i = 0; $i < @restrict; $i+=2) {
95 0         0 my $match = $restrict[$i];
96 0         0 my $more = int($restrict[$i+1])+1;
97 0 0       0 redo WORD if $res =~ m/([\Q$match\E].*){$more,}/;
98             }
99             }
100 505017         1196187 return $res;
101             }
102              
103             =head2 word
104              
105             $word = word($minlen, $maxlen [, $lang [, $numbers [, $caps [, $minfreq, $avgfreq ] ] ] );
106             $word = word3($minlen, $maxlen [, $lang [, $numbers [, $caps [, $minfreq, $avgfreq ] ] ] );
107              
108             Generates a random pronounceable word. The length of the returned
109             word will be between $minlen and $maxlen. If you supply a non-zero
110             value for $numbers, up to that many numbers and special characters
111             will occur in the password. If you specify a non-zero value for $caps,
112             up to this many characters will be upper case. $lang is the language
113             description to use, loaded via load_language or built-in. Built-in
114             languages are: 'en' (english) and 'de' (german). Contributions
115             welcome. The default language is 'en' but may be changed by calling
116             load_language with a true value as third parameter. Pass undef as
117             language to select the current default language. $minfreq and $minsum
118             determine quality of the password: $minfreq and $avgfreq are the minimum
119             frequency each quad/trigram must have and the average frequency that the
120             quad/trigrams must have for a word to be selected. Both are values between 0.0
121             and 1.0, specifying the percentage of the maximum frequency. Higher
122             values create less secure, better pronounceable passwords and are slower.
123             Useful $minfreq values are usually between 0.001 and 0.0001, useful $avgfreq
124             values are around 0.05 for trigrams (word3) and 0.001 for quadgrams (word).
125              
126             =cut
127              
128 4     4   23 use vars qw($total);
  4         7  
  4         7769  
129              
130             sub word($$;$$$$$)
131             {
132 10   50 10 1 6906 my $language = splice(@_,2,1) || '';
133 10         22 $language =~ s/[^a-zA-Z_]//g;
134 10   33     47 $language ||= $default_language;
135 10         976 eval "require Crypt::GeneratePassword::$language";
136 10         55 my $lang = $languages{$language};
137 10 50       105 die "language '${language}' not found" if !$lang;
138              
139 10         31 my ($minlen, $maxlen, $numbers, $capitals, $minfreq, $avgfreq) = map { int($_) } @_;
  20         59  
140 10   50     64 $minfreq ||= 0;
141 10   50     62 $avgfreq ||= 0.001;
142 10   50     61 $minfreq = int($$lang{'maxquad'}*$minfreq) || 1;
143 10         24 $avgfreq = int($$lang{'maxquad'}*$avgfreq);
144              
145 51220 50       242070 WORD: {
    50          
    50          
    50          
146 10         16 my $randword = chars($minlen,$maxlen,$set[$numbers?1:0][$capitals?1:0],($numbers?($signs,$numbers):()),($capitals?($caps,$capitals):()));
147 51220         74517 $total++;
148 51220         82574 my $stripped = lc($randword);
149 51220         190550 $stripped =~ s/[\Q$signs\E]//g;
150 51220 50       116571 redo WORD if length($stripped) == 0;
151              
152 51220         56884 my $sum = 0;
153 51220         53634 my $k0 = -1;
154 51220         50101 my $k1 = -1;
155 51220         55224 my $k2 = -1;
156 51220         54170 my $k3 = -1;
157              
158 51220         186225 foreach my $char (split(//,$stripped)) {
159 218303         253651 $k3 = $char;
160 218303 50       363224 if ($k3 gt 'Z') {
161 218303         261075 $k3 = ord($k3) - ord('a');
162             } else {
163 0         0 $k3 = ord($k3) - ord('A');
164             }
165              
166 218303 100       412322 if ($k0 > 0) {
167 61478 100       375472 redo WORD if $$lang{'quads'}[$k0][$k1][$k2][$k3] < $minfreq;
168 10269         19880 $sum += $$lang{'quads'}[$k0][$k1][$k2][$k3];
169             }
170              
171 167094         178909 $k0 = $k1;
172 167094         179282 $k1 = $k2;
173 167094         228594 $k2 = $k3;
174             }
175 11 100       81 redo if $sum/length($stripped) < $avgfreq;
176 10 50       36 redo if (restrict($stripped,$language));
177 10         72 return $randword;
178             }
179             }
180              
181             sub word3($$;$$$$$)
182             {
183 2   50 2 0 806 my $language = splice(@_,2,1) || '';
184 2         6 $language =~ s/[^a-zA-Z_]//g;
185 2   33     11 $language ||= $default_language;
186 2         188 eval "require Crypt::GeneratePassword::$language";
187 2         13 my $lang = $languages{$language};
188 2 50       16 die "language '${language}' not found" if !$lang;
189              
190 2         7 my ($minlen, $maxlen, $numbers, $capitals, $minfreq, $avgfreq) = map { int($_) } @_;
  4         10  
191 2   50     16 $minfreq ||= 0.01;
192 2   50     9 $avgfreq ||= 0.05;
193 2   50     11 $minfreq = int($$lang{'maxtri'}*$minfreq) || 1;
194 2         6 $avgfreq = int($$lang{'maxtri'}*$avgfreq);
195              
196 453787 50       2041049 WORD: {
    50          
    50          
    50          
197 2         4 my $randword = chars($minlen,$maxlen,$set[$numbers?1:0][$capitals?1:0],($numbers?($signs,$numbers):()),($capitals?($caps,$capitals):()));
198 453787         629173 $total++;
199 453787         668173 my $stripped = lc($randword);
200 453787         1465137 $stripped =~ s/[\Q$signs\E]//g;
201 453787 50       959449 redo WORD if length($stripped) == 0;
202              
203 453787         480596 my $sum = 0;
204 453787         495790 my $k1 = -1;
205 453787         443074 my $k2 = -1;
206 453787         422502 my $k3 = -1;
207              
208 453787         1733511 foreach my $char (split(//,$stripped)) {
209 1484988         1613039 $k3 = $char;
210 1484988 50       2340236 if ($k3 gt 'Z') {
211 1484988         1741674 $k3 = ord($k3) - ord('a');
212             } else {
213 0         0 $k3 = ord($k3) - ord('A');
214             }
215              
216 1484988 100       2674367 if ($k1 > 0) {
217 549169 100       2937183 redo WORD if $$lang{'tris'}[$k1][$k2][$k3] < $minfreq;
218 95384         172464 $sum += $$lang{'tris'}[$k1][$k2][$k3];
219             }
220              
221 1031203         1068522 $k1 = $k2;
222 1031203         1221825 $k2 = $k3;
223             }
224 2 50       14 redo if $sum/length($stripped) < $avgfreq;
225 2 50       7 redo if (restrict($stripped,$language));
226 2         14 return $randword;
227             }
228             }
229              
230             =head2 analyze
231              
232             $ratio = analyze($count,@word_params);
233             $ratio = analyze3($count,@word_params);
234              
235             Returns a statistical(!) security ratio to measure password
236             quality. $ratio is the ratio of passwords chosen among all
237             possible ones, e.g. a ratio of 0.0149 means 1.49% of the
238             theoretical password space was actually considered a
239             pronounceable password. Since this analysis is only
240             statistical, it proves absolutely nothing if you are deeply
241             concerned about security - but in that case you should use
242             chars(), not word() anyways. In reality, it says a lot
243             about your chosen parameters if you use large values for
244             $count.
245              
246             =cut
247              
248             sub analyze($@) {
249 0     0 1 0 my $count = shift;
250 0         0 $total = 0;
251 0         0 for (1..$count) {
252 0         0 my $word = &word(@_);
253             }
254 0         0 return $count/$total;
255             }
256              
257             sub analyze3($@) {
258 0     0 0 0 my $count = shift;
259 0         0 $total = 0;
260 0         0 for (1..$count) {
261 0         0 my $word = &word3(@_);
262             }
263 0         0 return $count/$total;
264             }
265              
266             =head2 generate_language
267              
268             $language_description = generate_language($wordlist);
269              
270             Generates a language description which can be saved in a file and/or
271             loaded with load_language. $wordlist can be a string containing
272             whitespace separated words, an array ref containing one word per
273             element or a file handle or name to read words from, one word per line7.
274             Alternatively, you may pass an array directly, not as reference.
275             A language description is about 1MB in size.
276              
277             If you generate a general-purpose language description for a
278             language not yet built-in, feel free to contribute it for inclusion
279             into this package.
280              
281             =cut
282              
283             sub generate_language($@) {
284 0     0 1 0 my ($wordlist) = @_;
285 0 0       0 if (@_ > 1) {
    0          
    0          
286 0         0 $wordlist = \@_;
287             } elsif (!ref($wordlist)) {
288 0         0 $wordlist = [ split(/\s+/,$wordlist) ];
289 0 0       0 if (@$wordlist == 1) {
290 0         0 local *FH;
291 0         0 open(FH,'<'.$$wordlist[0]);
292 0         0 $wordlist = [ ];
293 0         0 close(FH);
294             }
295             } elsif (ref($wordlist) ne 'ARRAY') {
296 0         0 $wordlist = [ <$wordlist> ];
297             }
298              
299 0         0 my @quads = map { [ map { [ map { [ map { 0 } 1..26 ] } 1..26 ] } 1..26 ] } 1..26;
  0         0  
  0         0  
  0         0  
  0         0  
300 0         0 my @tris = map { [ map { [ map { 0 } 1..26 ] } 1..26 ] } 1..26;
  0         0  
  0         0  
  0         0  
301 0         0 my $sigmaquad = 0;
302 0         0 my $maxquad = 0;
303 0         0 my $sigmatri = 0;
304 0         0 my $maxtri = 0;
305              
306 0         0 foreach my $word (@$wordlist) {
307 0         0 my $k0 = -1;
308 0         0 my $k1 = -1;
309 0         0 my $k2 = -1;
310 0         0 my $k3 = -1;
311              
312 0         0 foreach my $char (split(//,$word)) {
313 0         0 $k3 = $char;
314 0 0       0 if ($k3 gt 'Z') {
315 0         0 $k3 = ord($k3) - ord('a');
316             } else {
317 0         0 $k3 = ord($k3) - ord('A');
318             }
319              
320 0 0 0     0 next unless ($k3 >= 0 && $k3 <= 25);
321              
322 0 0       0 if ($k0 >= 0) {
323 0         0 $quads[$k0][$k1][$k2][$k3]++;
324 0         0 $sigmaquad++;
325 0 0       0 if ($quads[$k0][$k1][$k2][$k3] > $maxquad) {
326 0         0 $maxquad = $quads[$k0][$k1][$k2][$k3];
327             }
328             }
329              
330 0 0       0 if ($k1 >= 0) {
331 0         0 $tris[$k1][$k2][$k3]++;
332 0         0 $sigmatri++;
333 0 0       0 if ($tris[$k1][$k2][$k3] > $maxtri) {
334 0         0 $maxtri = $tris[$k1][$k2][$k3];
335             }
336             }
337              
338 0         0 $k0 = $k1;
339 0         0 $k1 = $k2;
340 0         0 $k2 = $k3;
341             }
342             }
343              
344             {
345 0         0 require Data::Dumper;
  0         0  
346 4     4   30 no warnings 'once';
  4         9  
  4         2520  
347 0         0 local $Data::Dumper::Indent = 0;
348 0         0 local $Data::Dumper::Purity = 0;
349 0         0 local $Data::Dumper::Pad = '';
350 0         0 local $Data::Dumper::Deepcopy = 1;
351 0         0 local $Data::Dumper::Terse = 1;
352              
353 0         0 my $res = Data::Dumper::Dumper(
354             {
355             maxtri => $maxtri,
356             sigmatri => $sigmatri,
357             maxquad => $maxquad,
358             sigmaquad => $sigmaquad,
359             tris => \@tris,
360             quads => \@quads,
361             }
362             );
363 0         0 $res =~ s/[' ]//g;
364 0         0 return $res;
365             }
366             }
367              
368             =head2 load_language
369              
370             load_language($language_description, $name [, $default]);
371              
372             Loads a language description which is then available in words().
373             $language_desription is a string returned by generate_language,
374             $name is a name of your choice which is used to select this
375             language as the fifth parameter of words(). You should use the
376             well-known ISO two letter language codes if possible, for best
377             interoperability.
378              
379             If you specify $default with a true value, this language will
380             be made global default language. If you give undef as
381             $language_description, only the default language will be changed.
382              
383             =cut
384              
385             sub load_language($$;$) {
386 0     0 1 0 my ($desc,$name,$default) = @_;
387 0 0       0 $languages{$name} = eval $desc if $desc;
388 0 0       0 $default_language = $name if $default;
389             }
390              
391             =head2 random_number
392              
393             $number = random_number($limit);
394              
395             Returns a random integer between 0 (inclusive) and $limit (exclusive).
396             Change this to a function of your choice by doing something like this:
397              
398             {
399             local $^W; # squelch sub redef warning.
400             *Crypt::GeneratePassword::random_number = \&my_rng;
401             }
402              
403             The default implementation uses perl's rand(), which might not be
404             appropriate for some sites.
405              
406             =cut
407              
408             sub random_number($) {
409 11275872     11275872 1 28572002 return int(rand()*$_[0]);
410             }
411              
412             =head2 restrict
413              
414             $forbidden = restrict($word,$language);
415              
416             Filters undesirable words. Returns false if the $word is allowed
417             in language $lang, false otherwise. Change this to a function of
418             your choice by doing something like this:
419              
420             {
421             local $^W; # squelch sub redef warning.
422             *Crypt::GeneratePassword::restrict = \&my_filter;
423             }
424              
425             The default implementation scans for a few letter sequences that
426             english or german people might find offending, mostly because of
427             their sexual nature. You might want to hook up a regular password
428             checker here, or a wordlist comparison.
429              
430             =cut
431              
432             sub restrict($$) {
433 12     12 1 167 return ($_[0] =~ m/f.ck|ass|rsch|tit|cum|ack|asm|orn|eil|otz|oes/i);
434             }
435              
436             =head1 SEE ALSO
437              
438             L
439              
440             =head1 REPOSITORY
441              
442             L
443              
444             =head1 AUTHOR
445              
446             Copyright 2002 by Jörg Walter ,
447             inspired by ideas from Tom Van Vleck and Morris
448             Gasser/FIPS-181.
449              
450             =head1 COPYRIGHT
451              
452             This perl module is free software; it may be redistributed and/or modified
453             under the same terms as Perl itself.
454              
455              
456             =cut