File Coverage

blib/lib/App/Genpass.pm
Criterion Covered Total %
statement 90 128 70.3
branch 15 40 37.5
condition 11 27 40.7
subroutine 15 18 83.3
pod 4 4 100.0
total 135 217 62.2


line stmt bran cond sub pod time code
1             package App::Genpass;
2             # ABSTRACT: Quickly and easily create secure passwords
3             $App::Genpass::VERSION = '2.401';
4 5     5   2203 use Carp;
  5         7  
  5         329  
5 5     5   2589 use Moo;
  5         55526  
  5         23  
6 5     5   8224 use Sub::Quote 'quote_sub';
  5         13182  
  5         287  
7 5     5   2616 use MooX::Types::MooseLike::Base qw/Int Str Bool ArrayRef/;
  5         25020  
  5         499  
8 5     5   3861 use Getopt::Long qw/:config no_ignore_case/;
  5         52945  
  5         28  
9 5     5   807 use File::Spec;
  5         7  
  5         89  
10 5     5   2069 use Config::Any;
  5         35427  
  5         165  
11 5     5   2716 use File::HomeDir;
  5         20930  
  5         290  
12 5     5   2584 use List::AllUtils qw( any none shuffle );
  5         54650  
  5         378  
13 5     5   2510 use Math::Random::Secure;
  5         534823  
  5         5498  
14              
15 89997     89997   227648 sub _rand ($) { Math::Random::Secure::rand(shift) }
16              
17             has uppercase => (
18             is => 'ro',
19             isa => ArrayRef,
20             default => quote_sub( q{ [ 'A' .. 'Z' ] } ),
21             );
22              
23             has lowercase => (
24             is => 'ro',
25             isa => ArrayRef,
26             default => quote_sub( q{ [ 'a' .. 'z' ] } ),
27             );
28              
29             has numerical => (
30             is => 'ro',
31             isa => ArrayRef,
32             default => quote_sub( q{ [ '0' .. '9' ] } ),
33             );
34              
35             has unreadable => (
36             is => 'ro',
37             isa => ArrayRef,
38             default => quote_sub( q{ [ split //sm, q{oO0l1I} ] } ),
39             );
40              
41             has specials => (
42             is => 'ro',
43             isa => ArrayRef,
44             default => quote_sub( q{ [ split //sm, q{!@#$%^&*()} ] } ),
45             );
46              
47             has number => (
48             is => 'ro',
49             isa => Int,
50             default => quote_sub( q{1} ),
51             );
52              
53             has readable => (
54             is => 'ro',
55             isa => Bool,
56             default => quote_sub( q{1} ),
57             );
58              
59             has verify => (
60             is => 'ro',
61             isa => Bool,
62             default => quote_sub( q{1} ),
63             );
64              
65             has length => (
66             is => 'ro',
67             isa => Int,
68             );
69              
70             has minlength => (
71             is => 'rw',
72             isa => Int,
73             default => quote_sub( q{8} ),
74             );
75              
76             has maxlength => (
77             is => 'rw',
78             isa => Int,
79             default => quote_sub( q{10} ),
80             );
81              
82             sub parse_opts {
83 0     0 1 0 my $class = shift;
84 0         0 my %opts = ();
85              
86             GetOptions(
87             'configfile=s' => \$opts{'configfile'},
88             'lowercase=s@' => \$opts{'lowercase'},
89             'uppercase=s@' => \$opts{'uppercase'},
90             'numerical=i@' => \$opts{'numerical'},
91             'unreadable=s@' => \$opts{'unreadable'},
92             'specials=s@' => \$opts{'specials'},
93             'n|number=i' => \$opts{'number'},
94             'r|readable!' => \$opts{'readable'},
95             'v|verify!' => \$opts{'verify'},
96             'l|length=i' => \$opts{'length'},
97             'm|minlength=i' => \$opts{'minlength'},
98 0 0       0 'x|maxlength=i' => \$opts{'maxlength'},
99             ) or croak q{Can't get options.};
100              
101             # remove undefined keys
102 0         0 foreach my $key ( keys %opts ) {
103 0 0       0 defined $opts{$key} or delete $opts{$key};
104             }
105              
106 0         0 return %opts;
107             }
108              
109             sub new_with_options {
110 0     0 1 0 my $class = shift;
111 0         0 my %opts = $class->parse_opts;
112 0         0 my @configs = (
113             File::Spec->catfile( File::HomeDir->my_home, '.genpass.yaml' ),
114             '/etc/genpass.yaml',
115             );
116              
117 0 0       0 if ( ! exists $opts{'configfile'} ) {
118 0         0 foreach my $file (@configs) {
119 0 0 0     0 if ( -e $file && -r $file ) {
120 0         0 $opts{'configfile'} = $file;
121 0         0 last;
122             }
123             }
124             }
125              
126 0 0       0 if ( exists $opts{'configfile'} ) {
127             %opts = (
128             %opts,
129 0         0 %{ $class->get_config_from_file( $opts{'configfile'} ) },
  0         0  
130             );
131             }
132              
133 0         0 my $self = $class->new( %opts, @_ );
134              
135 0         0 return $self;
136             }
137              
138             sub get_config_from_file {
139 0     0 1 0 my ($class, $file) = @_;
140              
141 0 0       0 $file = $file->() if ref $file eq 'CODE';
142 0 0       0 my $files_ref = ref $file eq 'ARRAY' ? $file : [$file];
143              
144 0         0 my $can_config_any_args = $class->can('config_any_args');
145 0 0       0 my $extra_args = $can_config_any_args ?
146             $can_config_any_args->($class, $file) : {};
147             ;
148 0         0 my $raw_cfany = Config::Any->load_files({
149             %$extra_args,
150             use_ext => 1,
151             files => $files_ref,
152             flatten_to_hash => 1,
153             } );
154              
155 0         0 my %raw_config;
156 0         0 foreach my $file_tested ( reverse @{$files_ref} ) {
  0         0  
157 0 0       0 if ( ! exists $raw_cfany->{$file_tested} ) {
158 0         0 warn qq{Specified configfile '$file_tested' does not exist, } .
159             qq{is empty, or is not readable\n};
160 0         0 next;
161             }
162              
163 0         0 my $cfany_hash = $raw_cfany->{$file_tested};
164 0 0 0     0 die "configfile must represent a hash structure in file: $file_tested"
      0        
165             unless $cfany_hash && ref $cfany_hash && ref $cfany_hash eq 'HASH';
166              
167 0         0 %raw_config = ( %raw_config, %{$cfany_hash} );
  0         0  
168             }
169              
170 0         0 \%raw_config;
171             }
172              
173             sub _get_chars {
174 5010     5010   11106 my $self = shift;
175 5010         5921 my @all_types = qw( lowercase uppercase numerical specials );
176 5010         3675 my @chars = ();
177 5010         3888 my @types = ();
178              
179             # adding all the combinations
180 5010         5387 foreach my $type (@all_types) {
181 20040 50       30937 if ( my $ref = $self->$type ) {
182 20040         11752 push @chars, @{$ref};
  20040         50756  
183 20040         20560 push @types, $type;
184             }
185             }
186              
187             # removing the unreadable chars
188 5010 100       7706 if ( $self->readable ) {
189             my @remove_chars = (
190 5007         5190 @{ $self->unreadable },
191 5007         3453 @{ $self->specials },
  5007         13439  
192             );
193              
194             @chars = grep {
195 5007         5322 local $a = $_;
  360368         256879  
196 360368     5165216   503090 none { $a eq $_ } @remove_chars;
  5165216         3060000  
197             } @chars;
198              
199             # removing specials
200 5007         12399 pop @types;
201             }
202              
203             # make both refs
204 5010         64336 return [ \@types, @chars ];
205             }
206              
207             sub generate {
208 5008     5008 1 55323 my ( $self, $number ) = @_;
209              
210 5008         3501 my $length;
211 5008         5945 my $verify = $self->verify;
212 5008         4643 my @passwords = ();
213 5008         3566 my @verifications = ();
214 5008         3648 my $EMPTY = q{};
215              
216 5008         3649 my ( $char_types, @chars ) = @{ $self->_get_chars };
  5008         6058  
217              
218 5008         16163 my @char_types = @{$char_types};
  5008         6092  
219 5008         4521 my $num_of_types = scalar @char_types;
220              
221 5008 50 66     107343 if ( (defined($self->length) && $num_of_types > $self->length)
      33        
222             || ($num_of_types > $self->minlength) ) {
223 0 0       0 $length = defined($self->length) ? $self->length : $self->minlength.' minimum';
224 0         0 croak <<"_DIE_MSG";
225             You wanted a shorter password that the variety of characters you've selected.
226             You requested $num_of_types types of characters but only have $length length.
227             _DIE_MSG
228             }
229              
230 5008 100       87860 if ($self->minlength > $self->maxlength) {
231 1         219 carp "minlength > maxlength, so I'm switching them";
232 1         113 my $min = $self->maxlength;
233 1         17 $self->maxlength($self->minlength);
234 1         59 $self->minlength($min);
235             }
236              
237 5008   66     135274 $length = $self->length
238             || $self->minlength + int(_rand(abs($self->maxlength - $self->minlength) + 1));
239              
240 5008   66     139461 $number ||= $self->number;
241              
242             # each password iteration needed
243 5008         7407 foreach my $pass_iter ( 1 .. $number ) {
244 5044         4368 my $password = $EMPTY;
245 5044         4226 my $char_type = shift @char_types;
246              
247             # generating the password
248 5044         7919 while ( $length > length $password ) {
249 42395         42308 my $char = $chars[ int _rand @chars ];
250              
251             # for verifying, we just check that it has small capital letters
252             # if that doesn't work, we keep asking it to get a new random one
253             # the check if it has large capital letters and so on
254 42395 100 66     641212 if ( $verify && $char_type && @{ $self->$char_type } ) {
  15143   66     39436  
255             # verify $char_type
256 15143 50       8351 if ( @{ $self->$char_type } ) {
  15143         21628  
257 15143     817101   27355 while ( ! any { $_ eq $char } @{ $self->$char_type } ) {
  817101         494332  
  58741         617638  
258 43598         64334 $char = $chars[ int _rand @chars ];
259             }
260             }
261              
262             $char_type =
263 15143 100       29344 scalar @char_types > 0 ? shift @char_types : $EMPTY;
264             }
265              
266 42395         63705 $password .= $char;
267             }
268              
269             # since the verification process creates a situation of ordered types
270             # (lowercase, uppercase, numerical, special)
271             # we need to shuffle the string
272 5044         18086 $password = join $EMPTY, shuffle( split //sm, $password );
273              
274 5044 100       29757 $number == 1 && return $password;
275              
276 40         43 push @passwords, $password;
277              
278 40         31 @char_types = @{$char_types};
  40         76  
279             }
280              
281 4 100       59 return wantarray ? @passwords : \@passwords;
282             }
283              
284              
285             1;
286              
287             __END__