File Coverage

blib/lib/PHP/Functions/Password.pm
Criterion Covered Total %
statement 187 216 86.5
branch 83 132 62.8
condition 54 120 45.0
subroutine 27 27 100.0
pod 10 10 100.0
total 361 505 71.4


line stmt bran cond sub pod time code
1             package PHP::Functions::Password;
2 6     6   12135 use strict;
  6         15  
  6         182  
3 6     6   28 use warnings;
  6         10  
  6         163  
4 6     6   26 use Carp qw(carp croak);
  6         41  
  6         384  
5 6     6   2439 use Crypt::Eksblowfish ();
  6         10552  
  6         126  
6 6     6   2254 use Crypt::OpenSSL::Random ();
  6         10031  
  6         190  
7 6     6   2226 use MIME::Base64 qw(encode_base64 decode_base64);
  6         3295  
  6         425  
8 6     6   2868 use Readonly qw(Readonly);
  6         22032  
  6         383  
9 6     6   2501 use version 0.77 ();
  6         10133  
  6         183  
10 6     6   50 use base qw(Exporter);
  6         14  
  6         1364  
11              
12             our @EXPORT;
13             our @EXPORT_OK = qw(
14             password_algos
15             password_get_info
16             password_hash
17             password_needs_rehash
18             password_verify
19             PASSWORD_BCRYPT
20             PASSWORD_ARGON2I
21             PASSWORD_ARGON2ID
22             PASSWORD_DEFAULT
23             );
24             our %EXPORT_TAGS = (
25             'all' => \@EXPORT_OK,
26             'default' => \@EXPORT,
27             'consts' => [ grep /^PASSWORD_/, @EXPORT_OK ],
28             'funcs' => [ grep /^password_/, @EXPORT_OK ],
29             );
30             our $VERSION = '1.12';
31              
32              
33             # Exported constants
34 6     6   43 use constant PASSWORD_BCRYPT => 1;
  6         14  
  6         557  
35 6     6   37 use constant PASSWORD_ARGON2I => 2;
  6         11  
  6         242  
36 6     6   32 use constant PASSWORD_ARGON2ID => 3;
  6         23  
  6         364  
37 6     6   32 use constant PASSWORD_DEFAULT => PASSWORD_BCRYPT;
  6         128  
  6         16575  
38              
39              
40             # Internal constants
41             Readonly my $PASSWORD_BCRYPT_DEFAULT_COST => 10; # no such PHP constant
42             Readonly my $PASSWORD_ARGON2_DEFAULT_SALT_LENGTH => 16; # no such PHP constant
43             Readonly my $PASSWORD_ARGON2_DEFAULT_MEMORY_COST => 65536;
44             Readonly my $PASSWORD_ARGON2_DEFAULT_TIME_COST => 4;
45             Readonly my $PASSWORD_ARGON2_DEFAULT_THREADS => 1;
46             Readonly my $PASSWORD_ARGON2_DEFAULT_TAG_LENGTH => 32; # no such PHP constant
47              
48             Readonly my $SIG_BCRYPT => '2y'; # PHP default
49             Readonly my $SIG_ARGON2I => 'argon2i';
50             Readonly my $SIG_ARGON2ID => 'argon2id';
51              
52             Readonly my %SIG_TO_ALGO => (
53             $SIG_BCRYPT => PASSWORD_BCRYPT,
54             $SIG_ARGON2I => PASSWORD_ARGON2I,
55             $SIG_ARGON2ID => PASSWORD_ARGON2ID,
56             );
57              
58             # https://en.wikipedia.org/wiki/Bcrypt
59             Readonly my $RE_BCRYPT_ALGO => qr#2[abxy]?#;
60             Readonly my $RE_BCRYPT_SALT => qr#[./A-Za-z0-9]{22}#; # fixed 16 byte salt
61             Readonly my $RE_BCRYPT_COST => qr#[0-3]\d#;
62             Readonly my $RE_BCRYPT_HASH => qr#[./A-Za-z0-9]+#;
63             Readonly my $RE_BCRYPT_STRING => qr/^
64             \$
65             ($RE_BCRYPT_ALGO) # $1 type
66             \$
67             ($RE_BCRYPT_COST) # $2 cost
68             \$
69             ($RE_BCRYPT_SALT) # $3 salt
70             ($RE_BCRYPT_HASH) # $4 hash
71             $/x;
72              
73             # See https://www.alexedwards.net/blog/how-to-hash-and-verify-passwords-with-argon2-in-go
74             Readonly my $RE_ARGON2_ALGO => qr#argon2id?#;
75             Readonly my $RE_ARGON2_STRING => qr/^
76             \$
77             ($RE_ARGON2_ALGO) # $1 signature
78             \$
79             v=(\d{1,3}) # $2 version
80             \$
81             m=(\d{1,10}), # $3 memory_cost
82             t=(\d{1,3}), # $4 time_cost
83             p=(\d{1,3}) # $5 threads
84             \$
85             ([A-Za-z0-9+\/]+) # $6 salt
86             \$
87             ([A-Za-z0-9+\/]+) # $7 hash
88             $/x;
89              
90             =head1 NAME
91              
92             PHP::Functions::Password - Perl ports of PHP password functions
93              
94             =head1 DESCRIPTION
95              
96             This module provides ported PHP password functions.
97             This module supports the bcrypt, argon2i, and argon2id algorithms, as is the case with the equivalent PHP functions at the date of writing this.
98             All functions may also be called as class methods and support inheritance too.
99             See L for detailed usage instructions.
100              
101             =head1 SYNOPSIS
102              
103             use PHP::Functions::Password ();
104              
105             Functional interface, typical use:
106              
107             use PHP::Functions::Password qw(password_hash);
108             my $password = 'secret';
109             my $crypted_string = password_hash($password); # uses PASSWORD_BCRYPT algorithm
110              
111             Functional interface use, using options:
112              
113             use PHP::Functions::Password qw(:all);
114             my $password = 'secret';
115              
116             # Specify options (see PHP docs for which):
117             my $crypted_string = password_hash($password, PASSWORD_DEFAULT, cost => 11);
118              
119             # Use a different algorithm:
120             my $crypted_string = password_hash($password, PASSWORD_ARGON2ID);
121              
122             Class method use, using options:
123              
124             use PHP::Functions::Password;
125             my $password = 'secret';
126             my $crypted_string = PHP::Functions::Password->hash($password, cost => 9);
127             # Note that the 2nd argument of password_hash() has been dropped here and may be specified
128             # as an option as should've been the case in the original password_hash() function IMHO.
129              
130             =head1 EXPORTS
131              
132             The following names can be imported into the calling namespace by request:
133              
134             password_algos
135             password_get_info
136             password_hash
137             password_needs_rehash
138             password_verify
139             PASSWORD_ARGON2I
140             PASSWORD_ARGON2ID
141             PASSWORD_BCRYPT
142             PASSWORD_DEFAULT
143             :all - what it says
144             :consts - the PASSWORD_* constants
145             :funcs - the password_* functions
146              
147             =head1 PHP COMPATIBLE AND EXPORTABLE FUNCTIONS
148              
149             =over
150              
151             =item password_algos()
152              
153             The same as L
154              
155             Returns an array of supported password algorithm signatures.
156              
157             =cut
158              
159             sub password_algos {
160 2     2 1 1057 my @result = ($SIG_BCRYPT);
161 2 50 33     16 if ($INC{'Crypt/Argon2.pm'} || eval { require Crypt::Argon2; }) {
  2         793  
162 2         1674 push(@result, $SIG_ARGON2I, $SIG_ARGON2ID);
163             }
164 2         23 return @result;
165             }
166              
167              
168              
169              
170             =item password_get_info($crypted)
171              
172             The same as L
173             with the exception that it returns the following additional keys in the result:
174              
175             algoSig e.g. '2y'
176             salt (encoded)
177             hash (encoded)
178             version (only for argon2 algorithms)
179              
180             Returns a hash in array context, else a hashref.
181              
182             =cut
183              
184             sub password_get_info {
185 34 100 66 34 1 3936 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
186 34         53 my $crypted = shift;
187 34 100       132 if ($crypted =~ $RE_BCRYPT_STRING) {
    100          
188 18         197 my $type = $1;
189 18         52 my $cost = int($2);
190 18         33 my $salt = $3;
191 18         31 my $hash = $4;
192 18         86 my %result = (
193             'algo' => PASSWORD_BCRYPT,
194             'algoName' => 'bcrypt',
195             'options' => {
196             'cost' => $cost,
197             },
198             'algoSig' => $type, # extra
199             'salt' => $salt, # extra
200             'hash' => $hash, # extra
201             );
202 18 100       102 return wantarray ? %result : \%result;
203             }
204             elsif ($crypted =~ $RE_ARGON2_STRING) {
205 10         166 my $sig = $1;
206 10         28 my $version = int($2);
207 10         18 my $memory_cost = int($3);
208 10         17 my $time_cost = int($4);
209 10         16 my $threads = int($5);
210 10         16 my $salt = $6;
211 10         13 my $hash = $7;
212             #my $raw_salt = decode_base64($salt);
213             #my $raw_hash = decode_base64($hash);
214             my %result = (
215 10         41 'algo' => $SIG_TO_ALGO{$sig},
216             'algoName' => $sig,
217             'options' => {
218             'memory_cost' => $memory_cost,
219             'time_cost' => $time_cost,
220             'threads' => $threads,
221             },
222             'algoSig' => $sig,
223             'salt' => $salt,
224             'hash' => $hash,
225             'version' => $version,
226             );
227 10 100       169 return wantarray ? %result : \%result;
228             }
229              
230             # No matches:
231 6         90 my %result = (
232             'algo' => 0,
233             'algoName' => 'unknown',
234             'options' => {},
235             );
236 6 100       28 return wantarray ? %result : \%result;
237             }
238              
239              
240              
241              
242             =item password_hash($password, $algo, %options)
243              
244             Similar to L
245             with difference that the $algo argument is optional and defaults to PASSWORD_DEFAULT for your programming pleasure.
246              
247             Important notes about the 'salt' option which you shouldn't use:
248              
249             - The PASSWORD_BCRYPT 'salt' option is deprecated since PHP 7.0, but if you do pass it, then it must be bcrypt custom base64 encoded and not raw bytes!
250             - For algorithms other than PASSWORD_BCRYPT, PHP doesn't support the 'salt' option, but if you do pass it, then it must be in raw bytes!
251              
252             =cut
253              
254             sub password_hash {
255 41 50 33 41 1 236 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
256 41         74 my $password = shift;
257 41   50     140 my $algo = shift // PASSWORD_DEFAULT;
258 41 50 66     199 my %options = @_ && ref($_[0]) ? %{$_[0]} : @_;
  0         0  
259 41 100 66     139 if ($algo == PASSWORD_BCRYPT) {
    50          
260 35         55 my $salt;
261 35 100 66     157 if (defined($options{'salt'}) && length($options{'salt'})) { # bcrypt custom base64 encoded!
262 32 50       170 unless ($options{'salt'} =~ /^$RE_BCRYPT_SALT$/) {
263 0         0 croak('Bad syntax in given and deprecated salt option (' . $options{'salt'} . ')');
264             }
265 32         377 $salt = $options{'salt'};
266             }
267             else {
268 3         64 $salt = $proto->_bcrypt_base64_encode(Crypt::OpenSSL::Random::random_bytes(16));
269             }
270 35   66     124 my $cost = $options{'cost'} || $PASSWORD_BCRYPT_DEFAULT_COST;
271 35         113 my $settings = '$' . $SIG_BCRYPT . '$' . sprintf('%.2u', $cost) . '$' . $salt;
272 35         480 return $proto->_bcrypt($password, $settings);
273             }
274             elsif (($algo == PASSWORD_ARGON2ID) || ($algo == PASSWORD_ARGON2I)) {
275 6 50 33     39 unless ($INC{'Crypt/Argon2.pm'} || eval { require Crypt::Argon2; }) {
  0         0  
276 0 0       0 my $algo_const_name = $algo == PASSWORD_ARGON2ID ? PASSWORD_ARGON2ID : PASSWORD_ARGON2I;
277 0         0 croak("Cannot use the $algo_const_name algorithm because the module Crypt::Argon2 is not installed");
278             }
279 6   33     62 my $salt = $options{'salt'} || Crypt::OpenSSL::Random::random_bytes($PASSWORD_ARGON2_DEFAULT_SALT_LENGTH); # undocumented; not a PHP option; raw!
280 6   33     2994 my $memory_cost = $options{'memory_cost'} || $PASSWORD_ARGON2_DEFAULT_MEMORY_COST;
281 6   33     70 my $time_cost = $options{'time_cost'} || $PASSWORD_ARGON2_DEFAULT_TIME_COST;
282 6   33     53 my $threads = $options{'threads'} || $PASSWORD_ARGON2_DEFAULT_THREADS;
283 6   33     48 my $tag_length = $options{'tag_length'} || $PASSWORD_ARGON2_DEFAULT_TAG_LENGTH; # undocumented; not a PHP option; 4 - 2^32 - 1
284              
285             # Ignore characters and treat strings as strings of bytes
286 6 100       51 utf8::is_utf8($password) && utf8::encode($password); # "\x{100}" becomes "\xc4\x80"; preferred equivalent of Encode::is_utf8($string) && Encode::_utf8_off($password);
287              
288 6         24 my @args = ($password, $salt, $time_cost, $memory_cost . 'k', $threads, $tag_length);
289 6 100       15 if ($algo == PASSWORD_ARGON2ID) {
290 3         1530857 return Crypt::Argon2::argon2id_pass(@args);
291             }
292             else {
293 3         1546218 return Crypt::Argon2::argon2i_pass(@args);
294             }
295             }
296             else {
297 0         0 croak("Unimplemented algorithm $algo");
298             }
299             }
300              
301              
302              
303              
304             =item password_needs_rehash($crypted, $algo, %options)
305              
306             The same as L.
307              
308             =cut
309              
310             sub password_needs_rehash {
311 24 100 66 24 1 6730 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
312 24         46 my $crypted = shift;
313 24   50     44 my $algo = shift(@_) // PASSWORD_DEFAULT;
314 24 50 33     68 my %options = @_ && ref($_[0]) ? %{$_[0]} : @_;
  24         71  
315 24         46 my %info = password_get_info($crypted);
316 24 100       60 unless ($info{'algo'} == $algo) {
317 2 50       5 $options{'debug'} && warn('Algorithms differ: ' . $info{'algo'} . "<>$algo");
318 2         6 return 1;
319             }
320 22 100 66     45 if ($algo == PASSWORD_BCRYPT) {
    100          
321 12 100       28 if ($info{'algoSig'} ne $SIG_BCRYPT) {
322 4 50       23 $options{'debug'} && warn('Algorithm signatures differ: ' . $info{'algoSig'} . ' vs ' . $SIG_BCRYPT);
323 4         26 return 1;
324             }
325 8   66     44 my $cost = $options{'cost'} // $PASSWORD_BCRYPT_DEFAULT_COST;
326 8 100 66     89 unless (defined($info{'options'}->{'cost'}) && ($info{'options'}->{'cost'} == $cost)) {
327 4 50       8 $options{'debug'} && warn('Cost mismatch: ' . $info{'options'}->{'cost'} . "<>$cost");
328 4         13 return 1;
329             }
330             }
331             elsif (($algo == PASSWORD_ARGON2ID) || ($algo == PASSWORD_ARGON2I)) {
332 8   33     18 my $memory_cost = $options{'memory_cost'} // $PASSWORD_ARGON2_DEFAULT_MEMORY_COST;
333 8 100       14 if ($info{'options'}->{'memory_cost'} != $memory_cost) {
334 2 50       5 $options{'debug'} && warn('memory_cost mismatch: ' . $info{'options'}->{'memory_cost'} . "<>$memory_cost");
335 2         7 return 1;
336             }
337 6   33     11 my $time_cost = $options{'time_cost'} // $PASSWORD_ARGON2_DEFAULT_TIME_COST;
338 6 100       10 if ($info{'options'}->{'time_cost'} != $time_cost) {
339 2 50       5 $options{'debug'} && warn('time_cost mismatch: ' . $info{'options'}->{'time_cost'} . "<>$time_cost");
340 2         8 return 1;
341             }
342 4   33     8 my $threads = $options{'threads'} // $PASSWORD_ARGON2_DEFAULT_THREADS;
343 4 100       7 if ($info{'options'}->{'threads'} != $threads) {
344 2 50       6 $options{'debug'} && warn('threads mismatch: ' . $info{'options'}->{'threads'} . "<>$threads");
345 2         7 return 1;
346             }
347 2 50 33     9 my $wanted_salt_length = defined($options{'salt'}) && length($options{'salt'}) ? length($options{'salt'}) : $PASSWORD_ARGON2_DEFAULT_SALT_LENGTH;
348 2   33     13 my $wanted_tag_length = $options{'tag_length'} || $PASSWORD_ARGON2_DEFAULT_TAG_LENGTH; # undocumented; not a PHP option; 4 - 2^32 - 1
349              
350 2 50 66     15 if ($INC{'Crypt/Argon2.pm'} || eval { require Crypt::Argon2; }) {
  1         555  
351 2 50       870 if (version->parse($Crypt::Argon2::VERSION) < version->parse('0.008')) {
352 0 0       0 if ($info{'version'} < 19) {
353 0 0       0 $options{'debug'} && warn('Version mismatch: ' . $info{'version'} . '<19');
354 0         0 return 1;
355             }
356 0         0 my $salt_encoded = $info{'salt'};
357 0         0 my $salt = decode_base64($salt_encoded);
358 0 0       0 if (!defined($salt)) {
359 0 0       0 $options{'debug'} && warn("decode_base64('$salt_encoded') failed");
360 0         0 return 1;
361             }
362 0         0 my $actual_salt_length = length($salt);
363 0 0       0 if ($wanted_salt_length != $actual_salt_length) {
364 0 0       0 $options{'debug'} && warn("wanted salt length ($wanted_salt_length) != actual salt length ($actual_salt_length)");
365 0         0 return 1;
366             }
367 0         0 my $tag_encoded = $info{'hash'};
368 0         0 my $tag = decode_base64($tag_encoded);
369 0         0 my $actual_tag_length = length($tag);
370 0 0       0 if ($wanted_tag_length != $actual_tag_length) {
371 0 0       0 $options{'debug'} && warn("wanted tag length ($wanted_tag_length) != actual tag length ($actual_tag_length)");
372 0         0 return 1;
373             }
374             }
375             else {
376 2         11 return Crypt::Argon2::argon2_needs_rehash($crypted, $info{'algoSig'}, $time_cost, $memory_cost . 'k', $threads, $wanted_tag_length, $wanted_salt_length);
377             }
378             }
379             }
380             else {
381 2 50       17 $options{'debug'} && warn("Can't do anything with unknown algorithm: $algo");
382             }
383 6         21 return 0;
384             }
385              
386              
387              
388              
389             =item password_verify($password, $crypted)
390              
391             The same as L.
392              
393             =cut
394              
395             sub password_verify {
396 50 100 66 50 1 17578 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
397 50         152 my ($password, $crypted) = @_;
398 50 100       322 if ($crypted =~ $RE_BCRYPT_STRING) {
    100          
399 32         575 my $cost = int($2);
400 32         83 my $salt = $3;
401 32         93 my $hash = $4;
402 32         123 my $new_crypt = $proto->hash(
403             $password,
404             'cost' => $cost,
405             'salt' => $salt,
406             );
407 32 100       175 if ($crypted eq $new_crypt) {
408 20         111 return 1;
409             }
410             # Since the signature may vary slightly, try comparing only the hash.
411 12   66     92 return ($new_crypt =~ $RE_BCRYPT_STRING) && ($4 eq $hash);
412             }
413             elsif ($crypted =~ $RE_ARGON2_STRING) {
414 16 50 33     500 unless ($INC{'Crypt/Argon2.pm'} || eval { require Crypt::Argon2; }) {
  0         0  
415             #carp("Verifying the $sig algorithm requires the module Crypt::Argon2 to be installed");
416 0         0 return 0;
417             }
418 16         137 my $algo = $SIG_TO_ALGO{$1};
419              
420             # Ignore characters and treat strings as strings of bytes
421 16 100       198 utf8::is_utf8($password) && utf8::encode($password); # "\x{100}" becomes "\xc4\x80"; preferred equivalent of Encode::is_utf8($string) && Encode::_utf8_off($password);
422              
423 16         39 my @args = ($crypted, $password);
424 16 100       41 if ($algo == PASSWORD_ARGON2ID) {
425 8         4302748 return Crypt::Argon2::argon2id_verify(@args);
426             }
427             else {
428 8         4142511 return Crypt::Argon2::argon2i_verify(@args);
429             }
430             }
431             #carp('Bad crypted argument');
432 2         35 return 0;
433             }
434              
435             =back
436              
437              
438              
439              
440             =head1 SHORTENED ALIAS METHODS
441              
442             =over
443              
444             =item algos()
445              
446             Alias of C.
447              
448             =cut
449              
450             sub algos {
451 1 50 33 1 1 18 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
452 1         3 return $proto->password_algos(@_);
453             }
454              
455              
456              
457              
458              
459             =item get_info($crypted)
460              
461             Alias of C.
462              
463             =cut
464              
465             sub get_info {
466 5 50 33 5 1 4486 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
467 5         17 return $proto->password_get_info(@_);
468             }
469              
470              
471              
472              
473             =item hash($password, %options)
474              
475             Proxy method for C.
476             The difference is that this method does have an $algo argument,
477             but instead allows the algorithm to be specified with the 'algo' option (in %options).
478              
479             =cut
480              
481             sub hash {
482 41 50 33 41 1 317 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
483 41         83 my $password = shift;
484 41 50 33     262 my %options = @_ && ref($_[0]) ? %{$_[0]} : @_;
  0         0  
485 41   100     173 my $algo = $options{'algo'} || PASSWORD_DEFAULT;
486 41         95 delete($options{'algo'});
487 41         179 return $proto->password_hash($password, $algo, %options);
488             }
489              
490              
491              
492              
493             =item needs_rehash($crypted, $algo, %options)
494              
495             Alias of C.
496              
497             =cut
498              
499             sub needs_rehash {
500 12 50 33 12 1 7644 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
501 12         33 return $proto->password_needs_rehash(@_);
502             }
503              
504              
505              
506              
507             =item verify($password, $crypted)
508              
509             Alias of C.
510              
511             =cut
512              
513             sub verify {
514 25 50 33 25 1 30042 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
515 25         128 return $proto->password_verify(@_);
516             }
517              
518             =back
519              
520             =cut
521              
522              
523              
524              
525             # From Crypt::Eksblowfish::Bcrypt.
526             # This is a version of C (see L) that implements the bcrypt algorithm.
527             sub _bcrypt {
528 35 50 33 35   194 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
529 35         78 my ($password, $settings) = @_;
530 35 50       147 unless ($settings =~ qr/^
531             \$
532             ($RE_BCRYPT_ALGO)
533             \$
534             ($RE_BCRYPT_COST)
535             \$
536             ($RE_BCRYPT_SALT)
537             /x) {
538 0         0 croak('Bad bcrypt settings argument');
539             }
540 35         853 my ($type, $cost, $salt_base64) = ($1, $2, $3);
541 35         154 my $hash = $proto->_bcrypt_hash(
542             $password,
543             {
544             'key_nul' => length($type) > 1,
545             'cost' => $cost,
546             'salt' => $proto->_bcrypt_base64_decode($salt_base64),
547             }
548             );
549 35         489 return '$' . $SIG_BCRYPT . '$' . $cost . '$' . $salt_base64 . $proto->_bcrypt_base64_encode($hash);
550             }
551              
552              
553              
554              
555             # From Crypt::Eksblowfish::Bcrypt.
556             # Hashes $password according to the supplied $settings, and returns the 23-octet hash.
557             sub _bcrypt_hash {
558 35 50 33 35   196 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
559 35         94 my ($password, $settings) = @_;
560 35 50 33     126 if ($settings->{'key_nul'} || ($password eq '')) {
561 35         91 $password .= "\0";
562             }
563              
564             # Ignore characters and treat strings as strings of bytes
565 35 100       167 utf8::is_utf8($password) && utf8::encode($password); # "\x{100}" becomes "\xc4\x80"; preferred equivalent of Encode::is_utf8($string) && Encode::_utf8_off($password);
566              
567             my $cipher = Crypt::Eksblowfish->new(
568             $settings->{'cost'},
569 35         3272421 $settings->{'salt'},
570             substr($password, 0, 72)
571             );
572             my $hash = join('',
573             map {
574 35         592 my $blk = $_;
  105         248  
575 105         333 for(my $i = 64; $i--; ) {
576 6720         14740 $blk = $cipher->encrypt($blk);
577             }
578 105         402 $blk;
579             } qw(OrpheanB eholderS cryDoubt)
580             );
581 35         139 chop($hash);
582 35         385 return $hash;
583             }
584              
585              
586              
587              
588             # From Crypt::Eksblowfish::Bcrypt.
589             # Decodes an octet string that was textually encoded using the form of base64 that is conventionally used with bcrypt.
590             sub _bcrypt_base64_decode {
591 35 50 33 35   203 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
592 35         68 my $text = shift;
593 35 50       307 unless ($text =~ m!\A(?>(?:[./A-Za-z0-9]{4})*)(?:|[./A-Za-z0-9]{2}[.CGKOSWaeimquy26]|[./A-Za-z0-9][.Oeu])\z!) {
594 0         0 croak('Bad base64 encoded text argument');
595             }
596 35         104 $text =~ tr#./A-Za-z0-9#A-Za-z0-9+/#;
597 35         168 $text .= '=' x (3 - (length($text) + 3) % 4);
598 35         274 return decode_base64($text);
599             }
600              
601              
602              
603              
604             # From Crypt::Eksblowfish::Bcrypt.
605             # Encodes the octet string textually using the form of base64 that is conventionally used with bcrypt.
606             sub _bcrypt_base64_encode {
607 38 50 33 38   905 my $proto = @_ && UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
608 38         116 my $octets = shift;
609 38         230 my $text = encode_base64($octets, '');
610 38         162 $text =~ tr#A-Za-z0-9+/=#./A-Za-z0-9#d; # "=" padding is deleted
611 38         470 return $text;
612             }
613              
614              
615              
616             1;
617              
618             __END__