File Coverage

lib/CtrlO/Crypt/XkcdPassword.pm
Criterion Covered Total %
statement 102 102 100.0
branch 22 22 100.0
condition 11 13 84.6
subroutine 22 22 100.0
pod 2 2 100.0
total 159 161 98.7


line stmt bran cond sub pod time code
1             package CtrlO::Crypt::XkcdPassword;
2 7     7   613751 use strict;
  7         75  
  7         190  
3 7     7   33 use warnings;
  7         12  
  7         158  
4 7     7   134 use 5.010;
  7         23  
5              
6             # ABSTRACT: Yet another xkcd style password generator
7              
8             our $VERSION = '1.011'; # VERSION
9              
10 7     7   37 use Carp qw(croak);
  7         11  
  7         411  
11 7     7   2832 use Crypt::Rijndael;
  7         2891  
  7         199  
12 7     7   2945 use Crypt::URandom;
  7         31902  
  7         321  
13 7     7   3305 use Data::Entropy qw(with_entropy_source);
  7         26873  
  7         425  
14 7     7   3320 use Data::Entropy::Algorithms qw(rand_int pick_r shuffle_r choose_r);
  7         72327  
  7         742  
15 7     7   3690 use Data::Entropy::RawSource::CryptCounter;
  7         15028  
  7         222  
16 7     7   3428 use Data::Entropy::Source;
  7         9612  
  7         239  
17 7     7   3006 use Data::Handle;
  7         221366  
  7         408  
18 7     7   56 use Module::Runtime qw(use_module);
  7         13  
  7         46  
19              
20 7     7   404 use base 'Class::Accessor::Fast';
  7         13  
  7         3399  
21              
22             __PACKAGE__->mk_accessors(qw(entropy wordlist language _list _pid));
23              
24              
25             sub new {
26 22     22 1 47121 my ( $class, %args ) = @_;
27              
28 22         47 my %object;
29              
30             # init the word list
31             my @list;
32 22 100       66 if ( $args{wordlist} ) {
33 16         44 $object{wordlist} = $args{wordlist};
34             }
35             else {
36 6   100     77 my $lang = lc( $args{language} || 'en-GB' );
37 6         26 $lang =~ s/-/_/g;
38 6         31 $object{wordlist} = 'CtrlO::Crypt::XkcdPassword::Wordlist::' . $lang;
39             }
40              
41 22 100       760 if ( -r $object{wordlist} ) {
    100          
    100          
42 3     1   153 open( my $fh, '<:encoding(UTF-8)', $object{wordlist} );
  1         7  
  1         3  
  1         7  
43 3         12692 while ( my $word = <$fh> ) {
44 18         60 chomp($word);
45 18         32 $word =~ s/\s//g;
46 18         76 push( @list, $word );
47             }
48 3         66 $object{_list} = \@list;
49             }
50             elsif ( $object{wordlist} =~ /::/ ) {
51 14         62 _load_wordlist_from_package( \%object );
52             }
53             elsif ( $object{wordlist} =~ /^\w+$/i ) {
54 4         25 $object{wordlist} = 'CtrlO::Crypt::XkcdPassword::Wordlist::' . $object{wordlist};
55 4         39 _load_wordlist_from_package( \%object );
56             }
57             else {
58             croak( 'Invalid word list: >'
59             . $object{wordlist}
60 1         40 . '<. Has to be either a Perl module or a file' );
61             }
62              
63             # poor person's lazy_build
64 18   66     4663 $object{entropy} = $args{entropy} || $class->_build_entropy;
65 18         9428 $object{_pid} = $$;
66              
67 18         160 return bless \%object, $class;
68             }
69              
70             sub _load_wordlist_from_package {
71 18     18   52 my ( $object ) = @_;
72 18         28 eval { use_module( $object->{wordlist} ); };
  18         105  
73 18 100       7563 if ($@) {
74 2         33 croak( "Cannot load word list module " . $object->{wordlist} );
75             }
76 16         43 my $pkg = $object->{wordlist};
77 7     7   20238 no strict 'refs';
  7         16  
  7         3713  
78              
79             # do we have a __DATA__ section, indication a subclass of https://metacpan.org/release/WordList
80 16         27 my $handle = eval { Data::Handle->new($pkg) };
  16         143  
81 16 100       12942 if ($handle) {
    100          
82 13         405 $object->{_list} = [ map { s/\n//g; chomp; $_ } $handle->getlines ];
  67239         148077  
  67239         70329  
  67239         80513  
83             }
84             # do we have @Words, indication Crypt::Diceware
85 3         18 elsif ( @{"${pkg}::Words"} ) {
86 2         3 $object->{_list} = \@{"${pkg}::Words"};
  2         8  
87             }
88             else {
89 1         10 croak("Cannot find word list in Perl module $pkg");
90             }
91             }
92              
93             sub _build_entropy {
94 15     15   87 my $class = shift;
95 15         126 return Data::Entropy::Source->new(
96             Data::Entropy::RawSource::CryptCounter->new(
97             Crypt::Rijndael->new( Crypt::URandom::urandom(32) )
98             ),
99             "getc"
100             );
101             }
102              
103              
104             sub xkcd {
105 45     45 1 5533850 my ( $self, %args ) = @_;
106 45 100       1815 if ( $self->_pid != $$ ) {
107 2         89 $self->_reinit_after_fork;
108             }
109              
110 45         2173 foreach my $key (keys %args) {
111 43 100 100     372 croak "Invalid key [$key] received."
112             unless ($key eq 'words' || $key eq 'digits');
113              
114 41 100 66     456 if (defined $args{$key} && ($args{$key} !~ /^[0-9]+$/)) {
115 12         138 croak "Invalid value [$args{$key}] for key [$key].";
116             }
117             }
118              
119 31   100     176 my $word_count = $args{words} || 4;
120              
121             my $words = with_entropy_source(
122             $self->entropy,
123             sub {
124 31     31   1298 shuffle_r( choose_r( $word_count, $self->_list ) );
125             }
126 31         723 );
127              
128 31 100       6814655 if ( my $d = $args{digits} ) {
129             push(
130             @$words,
131             sprintf(
132             '%0' . $d . 'u',
133             with_entropy_source(
134 10     10   254 $self->entropy, sub { rand_int( 10**$d ) }
135             )
136 10         557 )
137             );
138             }
139 31         1322 return join( '', map {ucfirst} @$words );
  105         619  
140             }
141              
142             sub _reinit_after_fork {
143 2     2   16 my $self = shift;
144 2         106 $self->_pid($$);
145 2         45 $self->entropy( $self->_build_entropy );
146             }
147              
148             'correct horse battery staple';
149              
150             __END__