File Coverage

blib/lib/App/bmkpasswd.pm
Criterion Covered Total %
statement 89 107 83.1
branch 46 76 60.5
condition 13 28 46.4
subroutine 17 19 89.4
pod 4 8 50.0
total 169 238 71.0


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