File Coverage

blib/lib/App/bmkpasswd.pm
Criterion Covered Total %
statement 86 105 81.9
branch 44 74 59.4
condition 13 28 46.4
subroutine 17 19 89.4
pod 4 8 50.0
total 164 234 70.0


line stmt bran cond sub pod time code
1             package App::bmkpasswd;
2             $App::bmkpasswd::VERSION = '2.011006';
3 8     8   1081358 use strictures 2;
  8         9032  
  8         321  
4 8     8   1296 use Carp;
  8         10  
  8         544  
5 8     8   180768 use Try::Tiny;
  8         9422  
  8         518  
6              
7 8         512 use Crypt::Eksblowfish::Bcrypt qw/
8             bcrypt
9             en_base64
10 8     8   3998 /;
  8         33726  
11              
12 8     8   47 use parent 'Exporter::Tiny';
  8         11  
  8         25  
13             our @EXPORT = qw/
14             mkpasswd
15             passwdcmp
16             /;
17             our @EXPORT_OK = qw/
18             mkpasswd_available
19             mkpasswd_forked
20             /;
21              
22 8     8   30470 use Bytes::Random::Secure::Tiny;
  8         61649  
  8         84482  
23             my ($brs, $brsnb);
24             sub get_brs {
25 18     18 0 23457 my (%params) = @_;
26             $params{strong} ?
27 18 50 0     124 $brs ||= Bytes::Random::Secure::Tiny->new(Bits => 128, NonBlocking => 0)
      66        
28             : $brsnb ||= Bytes::Random::Secure::Tiny->new(Bits => 128, NonBlocking => 1)
29             }
30              
31             sub mkpasswd_forked {
32 2     2 1 119 srand; # wrt random-length salts
33 2         4 undef $brs;
34 2         24 undef $brsnb;
35             }
36              
37             my %_can_haz;
38             sub have_passwd_xs {
39 25 100   25 0 302 unless (defined $_can_haz{passwdxs}) {
40 5     5   3523 try { require Crypt::Passwd::XS; $_can_haz{passwdxs} = 1 }
  5         1329  
41 5     0   66 catch { $_can_haz{passwdxs} = 0 };
  0         0  
42             }
43             $_can_haz{passwdxs}
44 25         168 }
45              
46             my %_shatests = (
47             sha256 => sub {
48             my $testc = try { crypt('a', '$5$abc$') } catch { warn $_; () };
49             $testc && index($testc, '$5$abc$') == 0 ? 1 : ()
50             },
51             sha512 => sub {
52             my $testc = try { crypt('b', '$6$abc$') } catch { warn $_; () };
53             $testc && index($testc, '$6$abc$') == 0 ? 1 : ()
54             },
55             );
56              
57             sub have_sha {
58             # if we have Crypt::Passwd::XS, just use that:
59 8 50   8 0 15 return 1 if have_passwd_xs();
60             # else determine (the slow way) if SHA256/512 are available via libc:
61 0   0     0 my $rate = $_[0] || 512;
62 0         0 my $type = "sha$rate";
63 0 0       0 return $_can_haz{$type} if defined $_can_haz{$type};
64 0 0 0     0 if (exists $_shatests{$type} && $_shatests{$type}->()) {
65 0         0 return $_can_haz{$type} = 1
66             }
67 0         0 return $_can_haz{$type} = 0
68             }
69              
70             sub have_md5 {
71 2 50   2 0 6 return 1 if have_passwd_xs();
72 0 0       0 return $_can_haz{md5} if defined $_can_haz{md5};
73 0     0   0 my $testc = try { crypt('a', '$1$abcd$') } catch { warn $_; () };
  0         0  
  0         0  
  0         0  
74 0 0 0     0 if ($testc && index($testc, '$1$abcd$') == 0) {
75 0         0 return $_can_haz{md5} = 1
76             }
77 0         0 return $_can_haz{md5} = 0
78             }
79              
80              
81             sub mkpasswd_available {
82 5     5 1 30 my ($type) = @_;
83              
84 5 50       19 unless ($type) {
85             return (
86 0 0       0 'bcrypt',
    0          
    0          
87             ( have_sha(256) ? 'sha256' : () ),
88             ( have_sha(512) ? 'sha512' : () ),
89             ( have_md5() ? 'md5' : () ),
90             );
91             }
92              
93 5         15 $type = lc $type;
94 5 100       23 return 1 if $type eq 'bcrypt';
95 4 100       46 return have_sha($1) if $type =~ /^sha-?(\d{3})$/;
96 1 50       8 return have_md5() if $type eq 'md5';
97             return
98 0         0 }
99              
100             my $_saltgen = sub {
101             my ($type, $strong) = @_;
102              
103             my $rnd = get_brs(strong => $strong);
104              
105             if ($type eq 'bcrypt') {
106             return en_base64( $rnd->bytes(16) );
107             }
108              
109             if ($type eq 'sha') {
110             my $max = en_base64( $rnd->bytes(16) );
111             my $initial = substr $max, 0, 8, '';
112             # Drepper recommends random-length salts:
113             $initial .= substr $max, 0, 1, '' for 1 .. rand 8;
114             return $initial
115             }
116              
117             if ($type eq 'md5') {
118             return en_base64( $rnd->bytes(6) );
119             }
120              
121             confess "_saltgen fell through, unknown type $type"
122             };
123              
124             sub mkpasswd {
125             # mkpasswd $passwd => $type, $cost, $strongsalt;
126             # mkpasswd $passwd => +{
127             # type => $type,
128             # cost => $cost,
129             # saltgen => $coderef,
130             # strong => $strongsalt,
131             # }
132 16     16 1 4059 my $pwd = shift;
133 16 50       49 croak "mkpasswd passed an undef password"
134             unless defined $pwd;
135              
136             my %opts =
137 2         9 ref $_[0] eq 'HASH' ? %{ $_[0] }
138 16 100       57 : map {; $_ => shift } qw/type cost strong/;
  42         85  
139 16 100       62 my $type = defined $opts{type} ? $opts{type} : 'bcrypt';
140 16   66     73 my $saltgen = $opts{saltgen} || $_saltgen;
141              
142 16         18 my $salt;
143              
144             TYPE: {
145 16 100       15 if ($type =~ /^bcrypt$/i) {
  16         72  
146 10   100     32 my $cost = $opts{cost} || '08';
147              
148 10 50       43 croak 'Work cost factor must be numeric'
149             unless $cost =~ /^[0-9]+$/;
150 10 100       32 $cost = "0$cost" if length $cost == 1;
151              
152 10         24 $salt = $saltgen->(bcrypt => $opts{strong});
153 10         396 my $bsettings = join '', '$2a$', $cost, '$', $salt;
154              
155             # bcrypt returns from here; everything else depends on have_passwd_xs
156 10         26 return bcrypt($pwd, $bsettings)
157             }
158              
159 6 100       21 if ($type =~ /^sha-?512$/i) {
160 2 50       4 croak 'SHA hash requested but no SHA support available'
161             unless have_sha(512);
162 2         4 $salt = join '', '$6$', $saltgen->(sha => $opts{strong}), '$';
163             last TYPE
164 2         3 }
165              
166 4 100       22 if ($type =~ /^sha(-?256)?$/i) {
167 3 50       5 croak 'SHA hash requested but no SHA support available'
168             unless have_sha(256);
169 3         9 $salt = join '', '$5$', $saltgen->(sha => $opts{strong}), '$';
170             last TYPE
171 3         596 }
172              
173 1 50       5 if ($type =~ /^md5$/i) {
174 1 50       2 croak 'MD5 hash requested but no MD5 support available'
175             unless have_md5;
176 1         4 $salt = join '', '$1$', $saltgen->(md5 => $opts{strong}), '$';
177             last TYPE
178 1         129 }
179              
180 0         0 croak "Unknown type specified: $type"
181             }
182              
183 6 50       10 have_passwd_xs() ?
184             Crypt::Passwd::XS::crypt($pwd, $salt) : crypt($pwd, $salt)
185             }
186              
187             sub _eq {
188 56     56   194393 my ($orig, $created) = @_;
189 56         98 my $unequal = ! (length $orig == length $created);
190 56         75 my $n = 0;
191 8     8   856 no warnings 'substr';
  8         1353  
  8         3137  
192 56         149 while ($n < length $orig) {
193 1462         1036 my $schr = substr $created, $n, 1;
194 1462 100       2067 $unequal = 1
    100          
195             if substr($orig, $n, 1) ne (defined $schr ? $schr : '');
196 1462         1622 ++$n;
197             }
198 56         256 ! $unequal
199             }
200              
201             sub passwdcmp {
202 23     23 1 48245 my ($pwd, $crypt) = @_;
203 23 50 33     129 croak 'Expected a password string and hash'
204             unless defined $pwd and $crypt;
205              
206 23         50 my $pos_a = index $crypt, '$';
207 23         51 my $pos_b = index $crypt, '$', 2;
208 23 100 100     329 carp 'Possibly passed an invalid hash'
      100        
209             unless $pos_a == 0
210             and $pos_b == 2
211             or $pos_b == 3;
212              
213 23 100       89 if ($crypt =~ /^\$2a\$\d{2}\$/) {
214             # Looks like bcrypt.
215 15 100       44 return $crypt if _eq( $crypt, bcrypt($pwd, $crypt) )
216             } else {
217 8 50       17 if (have_passwd_xs) {
218 8 100       20 return $crypt
219             if _eq( $crypt, Crypt::Passwd::XS::crypt($pwd, $crypt) )
220             } else {
221 0 0       0 return $crypt
222             if _eq( $crypt, crypt($pwd, $crypt) )
223             }
224             }
225              
226             undef
227 11         69 }
228              
229             1;
230             __END__