File Coverage

blib/lib/App/bmkpasswd.pm
Criterion Covered Total %
statement 86 104 82.6
branch 46 76 60.5
condition 13 28 46.4
subroutine 16 18 88.8
pod 4 8 50.0
total 165 234 70.5


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