File Coverage

blib/lib/Crypt/GeneratePassword.pm
Criterion Covered Total %
statement 94 169 55.6
branch 30 72 41.6
condition 12 28 42.8
subroutine 9 13 69.2
pod 7 9 77.7
total 152 291 52.2


line stmt bran cond sub pod time code
1             package Crypt::GeneratePassword;
2             $Crypt::GeneratePassword::VERSION = '0.05';
3             # ABSTRACT: generate secure random pronounceable passwords
4              
5 4     4   63109 use 5.006;
  4         14  
6 4     4   20 use strict;
  4         8  
  4         107  
7 4     4   35 use warnings;
  4         12  
  4         9220  
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             our %languages = ();
59              
60             =head2 chars
61              
62             $word = chars($minlen, $maxlen [, $set [, $characters, $maxcount ] ... ] );
63              
64             Generates a completely random word between $minlen and $maxlen in length.
65             If $set is given, it must be an array ref of characters to use. You can
66             restrict occurrence of some characters by providing ($characters, $maxcount)
67             pairs, as many as you like. $characters must be a string consisting of those
68             characters which may appear at most $maxcount times in the word.
69              
70             Note that the length is determined via relative probability, not uniformly.
71              
72             =cut
73              
74             my @signs = ('0'..'9', '%', '$', '_', '-', '+', '*', '&', '/', '=', '!', '#');
75             my $signs = join('',@signs);
76             my @caps = ('A' .. 'Z');
77             my $caps = join('',@caps);
78              
79             my @set = (
80             [ ["\x00",'a'..'z'], ["\x00",'a'..'z',@caps] ],
81             [ ["\x00",'a'..'z',@signs], ["\x00",'a'..'z',@caps,@signs] ]
82             );
83              
84             sub chars($$;$@) {
85 1317014     1317014 1 2048917 my ($minlen, $maxlen, $set, @restrict) = @_;
86 1317014   66     2742630 $set ||= $set[1][1];
87 1317014         1448771 my $res;
88 1317014         1689136 my $diff = $maxlen-$minlen;
89             WORD: {
90 1317014         1357424 $res = join '', map { $$set[random_number(scalar(@$set))] } 1..$maxlen;
  2135481         3855430  
  29601854         50580380  
91 2135481         11061087 $res =~ s/\x00{0,$diff}$//;
92 2135481 100       5458902 redo if $res =~ m/\x00/;
93 1317014         3687257 for (my $i = 0; $i < @restrict; $i+=2) {
94 0         0 my $match = $restrict[$i];
95 0         0 my $more = int($restrict[$i+1])+1;
96 0 0       0 redo WORD if $res =~ m/([\Q$match\E].*){$more,}/;
97             }
98             }
99 1317014         2528414 return $res;
100             }
101              
102             =head2 word
103              
104             $word = word($minlen, $maxlen [, $lang [, $numbers [, $caps [, $minfreq, $avgfreq ] ] ] );
105             $word = word3($minlen, $maxlen [, $lang [, $numbers [, $caps [, $minfreq, $avgfreq ] ] ] );
106              
107             Generates a random pronounceable word. The length of the returned
108             word will be between $minlen and $maxlen. If you supply a non-zero
109             value for $numbers, up to that many numbers and special characters
110             will occur in the password. If you specify a non-zero value for $caps,
111             up to this many characters will be upper case. $lang is the language
112             description to use, loaded via load_language or built-in. Built-in
113             languages are: 'en' (english) and 'de' (german). Contributions
114             welcome. The default language is 'en' but may be changed by calling
115             load_language with a true value as third parameter. Pass undef as
116             language to select the current default language. $minfreq and $minsum
117             determine quality of the password: $minfreq and $avgfreq are the minimum
118             frequency each quad/trigram must have and the average frequency that the
119             quad/trigrams must have for a word to be selected. Both are values between 0.0
120             and 1.0, specifying the percentage of the maximum frequency. Higher
121             values create less secure, better pronounceable passwords and are slower.
122             Useful $minfreq values are usually between 0.001 and 0.0001, useful $avgfreq
123             values are around 0.05 for trigrams (word3) and 0.001 for quadgrams (word).
124              
125             =cut
126              
127             our $total;
128              
129             sub word($$;$$$$$)
130             {
131 10   50 10 1 5826 my $language = splice(@_,2,1) || '';
132 10         21 $language =~ s/[^a-zA-Z_]//g;
133 10   33     45 $language ||= $default_language;
134 10         1181 eval "require Crypt::GeneratePassword::$language";
135 10         49 my $lang = $languages{$language};
136 10 50       53 die "language '${language}' not found" if !$lang;
137              
138 10         22 my ($minlen, $maxlen, $numbers, $capitals, $minfreq, $avgfreq) = map { int($_) } @_;
  20         47  
139 10   50     49 $minfreq ||= 0;
140 10   50     45 $avgfreq ||= 0.001;
141 10   50     48 $minfreq = int($$lang{'maxquad'}*$minfreq) || 1;
142 10         30 $avgfreq = int($$lang{'maxquad'}*$avgfreq);
143              
144             WORD: {
145 10 50       17 my $randword = chars($minlen,$maxlen,$set[$numbers?1:0][$capitals?1:0],($numbers?($signs,$numbers):()),($capitals?($caps,$capitals):()));
  97364 50       402277  
    50          
    50          
146 97364         132773 $total++;
147 97364         142384 my $stripped = lc($randword);
148 97364         273351 $stripped =~ s/[\Q$signs\E]//g;
149 97364 50       186347 redo WORD if length($stripped) == 0;
150              
151 97364         110628 my $sum = 0;
152 97364         113082 my $k0 = -1;
153 97364         113146 my $k1 = -1;
154 97364         104080 my $k2 = -1;
155 97364         104397 my $k3 = -1;
156              
157 97364         306616 foreach my $char (split(//,$stripped)) {
158 415442         476798 $k3 = $char;
159 415442 50       758628 if ($k3 gt 'Z') {
160 415442         513154 $k3 = ord($k3) - ord('a');
161             } else {
162 0         0 $k3 = ord($k3) - ord('A');
163             }
164              
165 415442 100       757549 if ($k0 > 0) {
166 117384 100       500452 redo WORD if $$lang{'quads'}[$k0][$k1][$k2][$k3] < $minfreq;
167 20032         32756 $sum += $$lang{'quads'}[$k0][$k1][$k2][$k3];
168             }
169              
170 318090         350464 $k0 = $k1;
171 318090         332119 $k1 = $k2;
172 318090         426363 $k2 = $k3;
173             }
174 12 100       62 redo if $sum/length($stripped) < $avgfreq;
175 10 50       24 redo if (restrict($stripped,$language));
176 10         70 return $randword;
177             }
178             }
179              
180             sub word3($$;$$$$$)
181             {
182 2   50 2 0 796 my $language = splice(@_,2,1) || '';
183 2         4 $language =~ s/[^a-zA-Z_]//g;
184 2   33     11 $language ||= $default_language;
185 2         200 eval "require Crypt::GeneratePassword::$language";
186 2         19 my $lang = $languages{$language};
187 2 50       23 die "language '${language}' not found" if !$lang;
188              
189 2         9 my ($minlen, $maxlen, $numbers, $capitals, $minfreq, $avgfreq) = map { int($_) } @_;
  4         16  
190 2   50     18 $minfreq ||= 0.01;
191 2   50     13 $avgfreq ||= 0.05;
192 2   50     15 $minfreq = int($$lang{'maxtri'}*$minfreq) || 1;
193 2         6 $avgfreq = int($$lang{'maxtri'}*$avgfreq);
194              
195             WORD: {
196 2 50       6 my $randword = chars($minlen,$maxlen,$set[$numbers?1:0][$capitals?1:0],($numbers?($signs,$numbers):()),($capitals?($caps,$capitals):()));
  1219640 50       4696659  
    50          
    50          
197 1219640         1584367 $total++;
198 1219640         1741480 my $stripped = lc($randword);
199 1219640         3614818 $stripped =~ s/[\Q$signs\E]//g;
200 1219640 50       2407729 redo WORD if length($stripped) == 0;
201              
202 1219640         1467926 my $sum = 0;
203 1219640         1452373 my $k1 = -1;
204 1219640         1305793 my $k2 = -1;
205 1219640         1400081 my $k3 = -1;
206              
207 1219640         4259742 foreach my $char (split(//,$stripped)) {
208 3992596         4936682 $k3 = $char;
209 3992596 50       6705111 if ($k3 gt 'Z') {
210 3992596         5083785 $k3 = ord($k3) - ord('a');
211             } else {
212 0         0 $k3 = ord($k3) - ord('A');
213             }
214              
215 3992596 100       7609022 if ($k1 > 0) {
216 1477377 100       5848597 redo WORD if $$lang{'tris'}[$k1][$k2][$k3] < $minfreq;
217 257741         407385 $sum += $$lang{'tris'}[$k1][$k2][$k3];
218             }
219              
220 2772960         3036581 $k1 = $k2;
221 2772960         3768000 $k2 = $k3;
222             }
223 4 100       44 redo if $sum/length($stripped) < $avgfreq;
224 2 50       11 redo if (restrict($stripped,$language));
225 2         23 return $randword;
226             }
227             }
228              
229             =head2 analyze
230              
231             $ratio = analyze($count,@word_params);
232             $ratio = analyze3($count,@word_params);
233              
234             Returns a statistical(!) security ratio to measure password
235             quality. $ratio is the ratio of passwords chosen among all
236             possible ones, e.g. a ratio of 0.0149 means 1.49% of the
237             theoretical password space was actually considered a
238             pronounceable password. Since this analysis is only
239             statistical, it proves absolutely nothing if you are deeply
240             concerned about security - but in that case you should use
241             chars(), not word() anyways. In reality, it says a lot
242             about your chosen parameters if you use large values for
243             $count.
244              
245             =cut
246              
247             sub analyze($@) {
248 0     0 1 0 my $count = shift;
249 0         0 $total = 0;
250 0         0 for (1..$count) {
251 0         0 my $word = &word(@_);
252             }
253 0         0 return $count/$total;
254             }
255              
256             sub analyze3($@) {
257 0     0 0 0 my $count = shift;
258 0         0 $total = 0;
259 0         0 for (1..$count) {
260 0         0 my $word = &word3(@_);
261             }
262 0         0 return $count/$total;
263             }
264              
265             =head2 generate_language
266              
267             $language_description = generate_language($wordlist);
268              
269             Generates a language description which can be saved in a file and/or
270             loaded with load_language. $wordlist can be a string containing
271             whitespace separated words, an array ref containing one word per
272             element or a file handle or name to read words from, one word per line7.
273             Alternatively, you may pass an array directly, not as reference.
274             A language description is about 1MB in size.
275              
276             If you generate a general-purpose language description for a
277             language not yet built-in, feel free to contribute it for inclusion
278             into this package.
279              
280             =cut
281              
282             sub generate_language($@) {
283 0     0 1 0 my ($wordlist) = @_;
284 0 0       0 if (@_ > 1) {
    0          
    0          
285 0         0 $wordlist = \@_;
286             } elsif (!ref($wordlist)) {
287 0         0 $wordlist = [ split(/\s+/,$wordlist) ];
288 0 0       0 if (@$wordlist == 1) {
289 0         0 local *FH;
290 0         0 open(FH,'<'.$$wordlist[0]);
291 0         0 $wordlist = [ ];
292 0         0 close(FH);
293             }
294             } elsif (ref($wordlist) ne 'ARRAY') {
295 0         0 $wordlist = [ <$wordlist> ];
296             }
297              
298 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  
299 0         0 my @tris = map { [ map { [ map { 0 } 1..26 ] } 1..26 ] } 1..26;
  0         0  
  0         0  
  0         0  
300 0         0 my $sigmaquad = 0;
301 0         0 my $maxquad = 0;
302 0         0 my $sigmatri = 0;
303 0         0 my $maxtri = 0;
304              
305 0         0 foreach my $word (@$wordlist) {
306 0         0 my $k0 = -1;
307 0         0 my $k1 = -1;
308 0         0 my $k2 = -1;
309 0         0 my $k3 = -1;
310              
311 0         0 foreach my $char (split(//,$word)) {
312 0         0 $k3 = $char;
313 0 0       0 if ($k3 gt 'Z') {
314 0         0 $k3 = ord($k3) - ord('a');
315             } else {
316 0         0 $k3 = ord($k3) - ord('A');
317             }
318              
319 0 0 0     0 next unless ($k3 >= 0 && $k3 <= 25);
320              
321 0 0       0 if ($k0 >= 0) {
322 0         0 $quads[$k0][$k1][$k2][$k3]++;
323 0         0 $sigmaquad++;
324 0 0       0 if ($quads[$k0][$k1][$k2][$k3] > $maxquad) {
325 0         0 $maxquad = $quads[$k0][$k1][$k2][$k3];
326             }
327             }
328              
329 0 0       0 if ($k1 >= 0) {
330 0         0 $tris[$k1][$k2][$k3]++;
331 0         0 $sigmatri++;
332 0 0       0 if ($tris[$k1][$k2][$k3] > $maxtri) {
333 0         0 $maxtri = $tris[$k1][$k2][$k3];
334             }
335             }
336              
337 0         0 $k0 = $k1;
338 0         0 $k1 = $k2;
339 0         0 $k2 = $k3;
340             }
341             }
342              
343             {
344 0         0 require Data::Dumper;
  0         0  
345 4     4   25 no warnings 'once';
  4         7  
  4         1832  
346 0         0 local $Data::Dumper::Indent = 0;
347 0         0 local $Data::Dumper::Purity = 0;
348 0         0 local $Data::Dumper::Pad = '';
349 0         0 local $Data::Dumper::Deepcopy = 1;
350 0         0 local $Data::Dumper::Terse = 1;
351              
352 0         0 my $res = Data::Dumper::Dumper(
353             {
354             maxtri => $maxtri,
355             sigmatri => $sigmatri,
356             maxquad => $maxquad,
357             sigmaquad => $sigmaquad,
358             tris => \@tris,
359             quads => \@quads,
360             }
361             );
362 0         0 $res =~ s/[' ]//g;
363 0         0 return $res;
364             }
365             }
366              
367             =head2 load_language
368              
369             load_language($language_description, $name [, $default]);
370              
371             Loads a language description which is then available in words().
372             $language_description is a string returned by generate_language,
373             $name is a name of your choice which is used to select this
374             language as the fifth parameter of words(). You should use the
375             well-known ISO two letter language codes if possible, for best
376             interoperability.
377              
378             If you specify $default with a true value, this language will
379             be made global default language. If you give undef as
380             $language_description, only the default language will be changed.
381              
382             =cut
383              
384             sub load_language($$;$) {
385 0     0 1 0 my ($desc,$name,$default) = @_;
386 0 0       0 $languages{$name} = eval $desc if $desc;
387 0 0       0 $default_language = $name if $default;
388             }
389              
390             =head2 random_number
391              
392             $number = random_number($limit);
393              
394             Returns a random integer between 0 (inclusive) and C<$limit> (exclusive).
395             Change this to a function of your choice by doing something like this:
396              
397             sub my_rng ($) {
398             ...
399             }
400              
401             {
402             # suppress warning about function being redefined
403             no warnings 'redefine';
404             *Crypt::GeneratePassword::random_number = \&my_rng;
405             }
406              
407             The default implementation uses perl's rand(),
408             which might not be appropriate for some sites.
409              
410             =cut
411              
412             sub random_number($) {
413 29601854     29601854 1 71748594 return int(rand()*$_[0]);
414             }
415              
416             =head2 restrict
417              
418             $forbidden = restrict($word,$language);
419              
420             Filters undesirable words. Returns false if the $word is allowed
421             in language $lang, false otherwise. Change this to a function of
422             your choice by doing something like this:
423              
424             sub my_filter ($$) {
425             ...
426             }
427              
428             {
429             no warnings 'redefine';
430             *Crypt::GeneratePassword::restrict = \&my_filter;
431             }
432              
433             The default implementation scans for a few letter sequences that
434             english or german people might find offending, mostly because of
435             their sexual nature. You might want to hook up a regular password
436             checker here, or a wordlist comparison.
437              
438             =cut
439              
440             sub restrict($$) {
441 12     12 1 147 return ($_[0] =~ m/f.ck|ass|rsch|tit|cum|ack|asm|orn|eil|otz|oes/i);
442             }
443              
444             =head1 SEE ALSO
445              
446             L
447              
448             =head1 REPOSITORY
449              
450             L
451              
452             =head1 AUTHOR
453              
454             Copyright 2002 by Jörg Walter ,
455             inspired by ideas from Tom Van Vleck and Morris
456             Gasser/FIPS-181.
457              
458             Now maintained by Neil Bowers Eneilb@cpan.orgE
459              
460             =head1 COPYRIGHT
461              
462             This perl module is free software; it may be redistributed and/or modified
463             under the same terms as Perl itself.
464              
465              
466             =cut
467              
468             1;