File Coverage

blib/lib/PHP/Functions/Password.pm
Criterion Covered Total %
statement 165 214 77.1
branch 84 158 53.1
condition 45 131 34.3
subroutine 26 27 96.3
pod 10 10 100.0
total 330 540 61.1


line stmt bran cond sub pod time code
1             package PHP::Functions::Password;
2 6     6   1957227 use strict;
  6         12  
  6         294  
3 6     6   33 use warnings;
  6         11  
  6         512  
4 6     6   58 use Carp qw(carp croak);
  6         13  
  6         552  
5 6     6   3414 use Crypt::Bcrypt ();
  6         48586  
  6         215  
6 6     6   3132 use Crypt::OpenSSL::Random ();
  6         24963  
  6         306  
7 6     6   56 use MIME::Base64 qw(decode_base64);
  6         12  
  6         464  
8 6     6   4134 use Readonly qw(Readonly);
  6         31015  
  6         571  
9 6     6   89 use base qw(Exporter);
  6         16  
  6         1840  
10              
11             our @EXPORT;
12             our @EXPORT_OK = qw(
13             password_algos
14             password_get_info
15             password_hash
16             password_needs_rehash
17             password_verify
18             PASSWORD_BCRYPT
19             PASSWORD_ARGON2I
20             PASSWORD_ARGON2ID
21             PASSWORD_DEFAULT
22             );
23             our %EXPORT_TAGS = (
24             'all' => \@EXPORT_OK,
25             'default' => \@EXPORT,
26             'consts' => [ grep /^PASSWORD_/, @EXPORT_OK ],
27             'funcs' => [ grep /^password_/, @EXPORT_OK ],
28             );
29             our $VERSION = '2.01';
30              
31              
32             # Exported constants
33 6     6   48 use constant PASSWORD_BCRYPT => 1;
  6         13  
  6         522  
34 6     6   54 use constant PASSWORD_ARGON2I => 2; # exists in PHP since version 7.2
  6         12  
  6         392  
35 6     6   36 use constant PASSWORD_ARGON2ID => 3; # exists in PHP since version 7.3
  6         13  
  6         361  
36 6     6   42 use constant PASSWORD_DEFAULT => PASSWORD_BCRYPT;
  6         33  
  6         23498  
37              
38              
39             # Internal constants
40             Readonly my $SIG_BCRYPT => '2y'; # PHP default; equivalent of 2b in non-PHP implementations
41             Readonly my $PASSWORD_BCRYPT_DEFAULT_COST => 10; # no such PHP constant
42             Readonly my $PASSWORD_BCRYPT_MAX_PASSWORD_LEN => 72; # no such PHP constant
43              
44             # https://en.wikipedia.org/wiki/Bcrypt
45             Readonly my $RE_BCRYPT_ALGO => qr#2[abxy]?#;
46             Readonly my $RE_BCRYPT_SALT => qr#[./A-Za-z0-9]{22}#; # fixed 16 byte salt (encoded as 22 bcrypt-custom-base64 chars)
47             Readonly my $RE_BCRYPT_COST => qr#[0-3]\d#;
48             Readonly my $RE_BCRYPT_HASH => qr#[./A-Za-z0-9]+#;
49             Readonly my $RE_BCRYPT_STRING => qr/^
50             \$
51             ($RE_BCRYPT_ALGO) # $1 type
52             \$
53             ($RE_BCRYPT_COST) # $2 cost
54             \$
55             ($RE_BCRYPT_SALT) # $3 salt
56             ($RE_BCRYPT_HASH) # $4 hash
57             $/x;
58              
59              
60             Readonly my $SIG_ARGON2I => 'argon2i';
61             Readonly my $SIG_ARGON2ID => 'argon2id';
62             Readonly my %ARGON2_SIG_TO_ALGO => (
63             $SIG_ARGON2I => PASSWORD_ARGON2I,
64             $SIG_ARGON2ID => PASSWORD_ARGON2ID,
65             );
66             Readonly my $PASSWORD_ARGON2_DEFAULT_SALT_LENGTH => 16; # no such PHP constant
67             Readonly my $PASSWORD_ARGON2_DEFAULT_MEMORY_COST => 65536;
68             Readonly my $PASSWORD_ARGON2_DEFAULT_TIME_COST => 4;
69             Readonly my $PASSWORD_ARGON2_DEFAULT_THREADS => 1;
70             Readonly my $PASSWORD_ARGON2_DEFAULT_TAG_LENGTH => 32; # no such PHP constant
71              
72             # See https://www.alexedwards.net/blog/how-to-hash-and-verify-passwords-with-argon2-in-go
73             Readonly my $RE_ARGON2_ALGO => qr#argon2id?#;
74             Readonly my $RE_ARGON2_STRING => qr/^
75             \$
76             ($RE_ARGON2_ALGO) # $1 signature
77             \$
78             v=(\d{1,3}) # $2 version
79             \$
80             m=(\d{1,10}), # $3 memory_cost
81             t=(\d{1,3}), # $4 time_cost
82             p=(\d{1,3}) # $5 threads
83             \$
84             ([A-Za-z0-9+\/]+) # $6 salt
85             \$
86             ([A-Za-z0-9+\/]+) # $7 hash
87             $/x;
88              
89             =head1 NAME
90              
91             PHP::Functions::Password - Perl ports of PHP password functions
92              
93             =head1 DESCRIPTION
94              
95             This module provides ported PHP password functions.
96             This module supports the bcrypt, argon2i, and argon2id algorithms, as is the case with the equivalent PHP functions at the date of writing this.
97             All functions may also be called as class methods and support inheritance too.
98             See L for detailed usage instructions.
99              
100             =head1 SYNOPSIS
101              
102             use PHP::Functions::Password ();
103              
104             PHP compatible functional interface, typical using defaults:
105              
106             use PHP::Functions::Password qw(password_hash);
107             my $password = 'secret';
108             my $crypted_string = password_hash($password); # uses PASSWORD_BCRYPT algorithm
109              
110             PHP compatible functional interface use, using options:
111              
112             use PHP::Functions::Password qw(:all);
113             my $password = 'secret';
114              
115             # Specify options (see PHP docs for which):
116             my $crypted_string = password_hash($password, PASSWORD_DEFAULT, cost => 11);
117              
118             # Use a different algorithm:
119             my $crypted_string = password_hash($password, PASSWORD_ARGON2ID);
120              
121             # Better practice using a 'pepper':
122             use Digest::SHA qw(hmac_sha256);
123             my $pepper = 'Abracadabra and Hocus pocus'; # retrieve this from a secrets config file for example (and don't loose it!)
124             my $peppered_password = hmac_sha256($password, $pepper);
125             my $crypted_string = password_hash($[peppered_password, PASSWORD_ARGON2ID); # store this in your database
126             # ... and when verifying passwords, then you must pepper them first.
127              
128             Static method use, using defaults:
129              
130             use PHP::Functions::Password;
131             my $password = 'secret';
132             my $crypted_string = PHP::Functions::Password->hash($password);
133              
134             Static method use, using options:
135              
136             use PHP::Functions::Password;
137             my $password = 'secret';
138             my $crypted_string = PHP::Functions::Password->hash($password, algo => PASSWORD_ARGON2ID, time_cost => 8);
139             # Note that the method hash() has a different argument signature compared to the function password_hash(). The algorithm has become one of the hash options.
140              
141             =head1 EXPORTS
142              
143             The following names can be imported into the calling namespace by request:
144              
145             password_algos
146             password_get_info
147             password_hash
148             password_needs_rehash
149             password_verify
150             PASSWORD_ARGON2I
151             PASSWORD_ARGON2ID
152             PASSWORD_BCRYPT
153             PASSWORD_DEFAULT
154             :all - what it says
155             :consts - the PASSWORD_* constants
156             :funcs - the password_* functions
157              
158             =cut
159              
160              
161             ### Protected methods that you may override in a subclass. ###
162              
163              
164             # See algos() and password_algos()
165             # The same as L
166             # Returns an array of supported password algorithm signatures.
167             sub _algos {
168 2     2   9 my @result = ($SIG_BCRYPT);
169 2 50 33     20 if ($INC{'Crypt/Argon2.pm'} || eval { require Crypt::Argon2; }) {
  2         1256  
170 2         2606 push(@result, $SIG_ARGON2I, $SIG_ARGON2ID);
171             }
172 2         30 return @result;
173             }
174              
175              
176              
177             # Called by get_info($crypted) and password_get_info($crypted)
178             # Similar to L with the difference that it returns the following additional keys in the result:
179             #
180             # algoSig e.g. '2y'
181             # salt (encoded)
182             # hash (encoded)
183             # version (only for argon2 algorithms)
184             #
185             # Returns a hashref if there is a match, else undef.
186             sub _get_info {
187 34 50 33 34   162 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
188 34         57 my $crypted = shift;
189 34 100       231 if ($crypted =~ $RE_BCRYPT_STRING) {
    100          
190 18         329 my $sig = $1;
191 18         60 my $cost = int($2);
192 18         48 my $salt = $3;
193 18         99 my $hash = $4;
194             return {
195 18         201 'algo' => PASSWORD_BCRYPT,
196             'algoName' => 'bcrypt',
197             'algoSig' => $sig, # extra
198             'options' => {
199             'cost'=> $cost,
200             },
201             'salt' => $salt, # extra
202             'hash' => $hash, # extra
203             };
204             }
205             elsif ($crypted =~ $RE_ARGON2_STRING) {
206 10         269 my $sig = $1;
207 10         36 my $version = int($2);
208 10         28 my $memory_cost = int($3);
209 10         42 my $time_cost = int($4);
210 10         22 my $threads = int($5);
211 10         25 my $salt = $6;
212 10         22 my $hash = $7;
213             #my $raw_salt = decode_base64($salt);
214             #my $raw_hash = decode_base64($hash);
215             return {
216 10         58 'algo' => $ARGON2_SIG_TO_ALGO{$sig},
217             'algoName' => $sig,
218             'algoSig' => $sig,
219             'options' => {
220             'memory_cost' => $memory_cost,
221             'time_cost' => $time_cost,
222             'threads' => $threads,
223             },
224             'salt' => $salt,
225             'hash' => $hash,
226             'version' => $version,
227             };
228              
229             }
230 6         95 return undef; # meaning no match
231             }
232              
233              
234              
235              
236             # _hash($password, %options)
237             # Similar to C but with a different argument signature.
238             # The difference is that this method doesn't have an $algo argument, but instead allows the algorithm to be specified using the 'algo' option (in %options).
239             # Important notes about the 'salt' option which you should avoid passing in the first place:
240             # - The PASSWORD_BCRYPT 'salt' option is deprecated since PHP 7.0, but if you do pass it, then it must be 16 bytes long!
241             # - 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!
242             sub _hash {
243 9 50 33 9   65 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
244 9         22 my $password = shift;
245 9 50 33     50 my %options = @_ && ref($_[0]) ? %{$_[0]} : @_;
  0         0  
246 9   50     37 my $algo = $options{'algo'} // PASSWORD_DEFAULT;
247 9 50       55 unless ($algo =~ /^\d$/) {
248 0         0 croak("Invalid \$algo parameter ($algo) which should be one of the PASSWORD_* integer constants");
249             }
250 9 100 66     48 if ($algo == PASSWORD_BCRYPT) {
    50          
251 3         6 my $salt;
252 3 50 33     17 if (defined($options{'salt'}) && length($options{'salt'})) {
253             # Treat salt as a string of bytes
254 0         0 $salt = $options{'salt'};
255 0 0       0 utf8::is_utf8($salt) && utf8::encode($salt); # "\x{100}" becomes "\xc4\x80"; preferred equivalent of Encode::is_utf8($string) && Encode::_utf8_off($password);
256 0 0       0 if (length($salt) == 16) {
    0          
257             # raw bytes: OK
258             }
259             elsif ($salt =~ /^$RE_BCRYPT_SALT$/) { # bcrypt-custom-base64 encoded string of 22 characters; DEPRECATED
260 0         0 $salt =~ tr#./A-Za-z0-9#A-Za-z0-9+/#;
261 0         0 $salt .= '=' x (3 - (length($salt) + 3) % 4);
262 0         0 $salt = decode_base64($salt);
263             }
264             else {
265 0         0 croak('Bad syntax in given and deprecated salt option (' . $options{'salt'} . ')');
266             }
267             }
268             else {
269 3         119 $salt = Crypt::OpenSSL::Random::random_bytes(16);
270             }
271 3         31 my $cost = $PASSWORD_BCRYPT_DEFAULT_COST;
272 3 50       27 if ($options{'cost'}) {
273 0         0 my $min_cost = 5;
274 0         0 my $max_cost = 31;
275 0 0 0     0 unless (($options{'cost'} =~ /^\d{1,2}$/) && ($options{'cost'} >= $min_cost) && ($options{'cost'} <= $max_cost)) {
      0        
276 0         0 croak('Invalid cost option given (' . $options{'cost'} . ") which should be an integer in the range $min_cost to $max_cost");
277             }
278 0         0 $cost = int($options{'cost'});
279             }
280              
281             # Treat passwords as strings of bytes
282 3 100       52 utf8::is_utf8($password) && utf8::encode($password); # "\x{100}" becomes "\xc4\x80"; preferred equivalent of Encode::is_utf8($string) && Encode::_utf8_off($password);
283              
284             # Everything beyond the max password length in bytes for bcrypt is silently ignored.
285 3         52 require bytes;
286 3 100       17 if (bytes::length($password) > $PASSWORD_BCRYPT_MAX_PASSWORD_LEN) { # $password is already bytes, so the bytes:: prefix is redundant here
287 1         60 $password = substr($password, 0, $PASSWORD_BCRYPT_MAX_PASSWORD_LEN);
288             }
289              
290 3         53 return Crypt::Bcrypt::bcrypt($password, $SIG_BCRYPT, $cost, $salt);
291             }
292             elsif (($algo == PASSWORD_ARGON2ID) || ($algo == PASSWORD_ARGON2I)) {
293 6 50 33     28 unless ($INC{'Crypt/Argon2.pm'} || eval { require Crypt::Argon2; }) {
  0         0  
294 0 0       0 my $algo_const_name = $algo == PASSWORD_ARGON2ID ? $SIG_ARGON2ID : $SIG_ARGON2I;
295 0         0 croak("Cannot use the $algo_const_name algorithm because the module Crypt::Argon2 is not installed");
296             }
297 6   33     107 my $salt = $options{'salt'} || Crypt::OpenSSL::Random::random_bytes($PASSWORD_ARGON2_DEFAULT_SALT_LENGTH); # undocumented; not a PHP option; raw!
298 6   33     4773 my $memory_cost = $options{'memory_cost'} || $PASSWORD_ARGON2_DEFAULT_MEMORY_COST;
299 6   33     66 my $time_cost = $options{'time_cost'} || $PASSWORD_ARGON2_DEFAULT_TIME_COST;
300 6   33     58 my $threads = $options{'threads'} || $PASSWORD_ARGON2_DEFAULT_THREADS;
301 6   33     57 my $tag_length = $options{'tag_length'} || $PASSWORD_ARGON2_DEFAULT_TAG_LENGTH; # undocumented; not a PHP option; 4 - 2^32 - 1
302              
303             # Treat passwords as strings of bytes
304 6 100       93 utf8::is_utf8($password) && utf8::encode($password); # "\x{100}" becomes "\xc4\x80"; preferred equivalent of Encode::is_utf8($string) && Encode::_utf8_off($password);
305              
306 6         29 my @args = ($password, $salt, $time_cost, $memory_cost . 'k', $threads, $tag_length);
307 6 100       18 if ($algo == PASSWORD_ARGON2ID) {
308 3         1166915 return Crypt::Argon2::argon2id_pass(@args);
309             }
310             else {
311 3         1170017 return Crypt::Argon2::argon2i_pass(@args);
312             }
313             }
314              
315 0         0 croak("Unimplemented algorithm $algo is probably not one of the known PASSWORD_* constants");
316             }
317              
318              
319              
320              
321             # _needs_rehash($crypted, $algo, %options)
322             # The same as L.
323             sub _needs_rehash {
324 24 50 33 24   129 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
325 24         42 my $crypted = shift;
326 24   50     79 my $algo = shift(@_) // PASSWORD_DEFAULT;
327 24 50 33     96 my %options = @_ && ref($_[0]) ? %{$_[0]} : @_;
  24         105  
328 24         75 my $info = $proto->_get_info($crypted);
329 24 100       189 unless ($info) {
330 4 50       14 $options{'debug'} && warn('Unrecognized format');
331 4         15 return 1;
332             }
333 20 50       61 unless ($info->{'algo'} == $algo) {
334 0 0       0 $options{'debug'} && warn('Algorithms differ: ' . $info->{'algo'} . "<>$algo");
335 0         0 return 1;
336             }
337 20 100 33     62 if ($algo == PASSWORD_BCRYPT) {
    50          
338             #unless (($info->{'algoSig'} eq $SIG_BCRYPT) || ($info->{'algoSig'} eq '2b')) { # also accept 2b as a non-PHP equivalent of 2y
339 12 100       40 unless ($info->{'algoSig'} eq $SIG_BCRYPT) { # this emulates PHP's behaviour (it requires 2b to be rehashed as 2y).
340 4 50       31 $options{'debug'} && warn('Algorithm signatures differ: ' . $info->{'algoSig'} . ' vs ' . $SIG_BCRYPT);
341 4         23 return 1;
342             }
343 8   66     71 my $cost = $options{'cost'} // $PASSWORD_BCRYPT_DEFAULT_COST;
344 8 100 66     103 unless (defined($info->{'options'}->{'cost'}) && ($info->{'options'}->{'cost'} == $cost)) {
345 4 50       18 $options{'debug'} && warn('Cost mismatch: ' . $info->{'options'}->{'cost'} . "<>$cost");
346 4         26 return 1;
347             }
348             }
349             elsif (($algo == PASSWORD_ARGON2ID) || ($algo == PASSWORD_ARGON2I)) {
350 8   33     21 my $memory_cost = $options{'memory_cost'} // $PASSWORD_ARGON2_DEFAULT_MEMORY_COST;
351 8 100       25 if ($info->{'options'}->{'memory_cost'} != $memory_cost) {
352 2 50       9 $options{'debug'} && warn('memory_cost mismatch: ' . $info->{'options'}->{'memory_cost'} . "<>$memory_cost");
353 2         14 return 1;
354             }
355 6   33     18 my $time_cost = $options{'time_cost'} // $PASSWORD_ARGON2_DEFAULT_TIME_COST;
356 6 100       19 if ($info->{'options'}->{'time_cost'} != $time_cost) {
357 2 50       7 $options{'debug'} && warn('time_cost mismatch: ' . $info->{'options'}->{'time_cost'} . "<>$time_cost");
358 2         13 return 1;
359             }
360 4   33     11 my $threads = $options{'threads'} // $PASSWORD_ARGON2_DEFAULT_THREADS;
361 4 100       14 if ($info->{'options'}->{'threads'} != $threads) {
362 2 50       7 $options{'debug'} && warn('threads mismatch: ' . $info->{'options'}->{'threads'} . "<>$threads");
363 2         15 return 1;
364             }
365 2 50 33     11 my $wanted_salt_length = defined($options{'salt'}) && length($options{'salt'}) ? length($options{'salt'}) : $PASSWORD_ARGON2_DEFAULT_SALT_LENGTH;
366 2   33     17 my $wanted_tag_length = $options{'tag_length'} || $PASSWORD_ARGON2_DEFAULT_TAG_LENGTH; # undocumented; not a PHP option; 4 - 2^32 - 1
367              
368 2 50 66     23 if ($INC{'Crypt/Argon2.pm'} || eval { require Crypt::Argon2; }) {
  1         657  
369 2 50       1314 if (Crypt::Argon2->can('argon2_needs_rehash')) { # since version 0.008
370 2         11 return Crypt::Argon2::argon2_needs_rehash($crypted, $info->{'algoSig'}, $time_cost, $memory_cost . 'k', $threads, $wanted_tag_length, $wanted_salt_length);
371             }
372             else { # as long as Crypt::Argon2 is not required for building, a minimum version requirement cannot be forced, and therefore the workaround below is needed
373 0 0       0 if ($info->{'version'} < 19) {
374 0 0       0 $options{'debug'} && warn('Version mismatch: ' . $info->{'version'} . '<19');
375 0         0 return 1;
376             }
377 0         0 my $salt_encoded = $info->{'salt'};
378 0         0 my $salt = decode_base64($salt_encoded);
379 0 0       0 if (!defined($salt)) {
380 0 0       0 $options{'debug'} && warn("decode_base64('$salt_encoded') failed");
381 0         0 return 1;
382             }
383 0         0 my $actual_salt_length = length($salt);
384 0 0       0 if ($wanted_salt_length != $actual_salt_length) {
385 0 0       0 $options{'debug'} && warn("wanted salt length ($wanted_salt_length) != actual salt length ($actual_salt_length)");
386 0         0 return 1;
387             }
388 0         0 my $tag_encoded = $info->{'hash'};
389 0         0 my $tag = decode_base64($tag_encoded);
390 0         0 my $actual_tag_length = length($tag);
391 0 0       0 if ($wanted_tag_length != $actual_tag_length) {
392 0 0       0 $options{'debug'} && warn("wanted tag length ($wanted_tag_length) != actual tag length ($actual_tag_length)");
393 0         0 return 1;
394             }
395             }
396             }
397             }
398             else {
399 0 0       0 $options{'debug'} && warn("Can't do anything with unknown algorithm: $algo");
400             }
401 4         26 return 0;
402             }
403              
404              
405              
406              
407             # _verify($password, $crypted)
408             # Similar to L, with the difference that undef is returned if the crypted string format is unrecognized.
409             sub _verify {
410 50 50 33 50   415 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
411 50         134 my $password = shift;
412 50         106 my $crypted = shift;
413 50 50 33     275 (defined($password) && length($password)) || croak('This first argument (password) must not be empty');
414 50 50 33     264 (defined($crypted) && length($crypted)) || croak('The second argument (crypted string) must not be empty');
415              
416 50 100       504 if ($crypted =~ $RE_BCRYPT_STRING) {
    100          
417              
418             # Treat passwords as strings of bytes
419 32 100       697 utf8::is_utf8($password) && utf8::encode($password); # "\x{100}" becomes "\xc4\x80"; preferred equivalent of Encode::is_utf8($string) && Encode::_utf8_off($password);
420              
421             # Everything beyond the max password length in bytes for bcrypt is silently ignored.
422 32         477 require bytes;
423 32 100       214 if (bytes::length($password) > $PASSWORD_BCRYPT_MAX_PASSWORD_LEN) { # $password is already bytes, so the bytes:: prefix is redundant here
424 10         227 $password = substr($password, 0, $PASSWORD_BCRYPT_MAX_PASSWORD_LEN);
425             }
426              
427 32 100       3163922 return Crypt::Bcrypt::bcrypt_check($password, $crypted) ? 1 : 0;
428             }
429              
430             elsif ($crypted =~ $RE_ARGON2_STRING) {
431 16 50 33     482 unless ($INC{'Crypt/Argon2.pm'} || eval { require Crypt::Argon2; }) {
  0         0  
432             #carp("Verifying the $sig algorithm requires the module Crypt::Argon2 to be installed");
433 0         0 return 0;
434             }
435 16         178 my $algo = $ARGON2_SIG_TO_ALGO{$1};
436              
437             # Treat passwords as strings of bytes
438 16 100       221 utf8::is_utf8($password) && utf8::encode($password); # "\x{100}" becomes "\xc4\x80"; preferred equivalent of Encode::is_utf8($string) && Encode::_utf8_off($password);
439              
440 16         54 my @args = ($crypted, $password);
441 16 100       91 if ($algo == PASSWORD_ARGON2ID) {
442 8 50       2926306 return Crypt::Argon2::argon2id_verify(@args) ? 1 : 0;
443             }
444             else {
445 8 50       3154098 return Crypt::Argon2::argon2i_verify(@args) ? 1 : 0;
446             }
447             }
448              
449 2         50 return undef; # meaning unrecognized format
450             }
451              
452              
453              
454              
455              
456              
457             =head1 PHP COMPATIBLE AND EXPORTABLE FUNCTIONS
458              
459             =over
460              
461             =item password_algos()
462              
463             The same as L
464              
465             Returns an array of supported password algorithm signatures.
466              
467             =cut
468              
469             sub password_algos {
470 1 50 33 1 1 1772 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
471 1         4 return $proto->_algos();
472             }
473              
474              
475              
476              
477             =item password_get_info($crypted)
478              
479             The same as L with the difference that it returns the following additional keys in the result:
480              
481             algoSig e.g. '2y'
482             salt (encoded)
483             hash (encoded)
484             version (only for argon2 algorithms)
485              
486             Returns a hash in array context, else a hashref.
487              
488             =cut
489              
490             sub password_get_info {
491 5 50 33 5 1 4972 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
492 5         22 my $info = $proto->_get_info(@_);
493              
494             # Emulate awkward PHP result when argument is unrecognized.
495 5 100       35 unless (defined($info)) {
496 1         9 $info = {
497             'algo' => 0,
498             'algoName' => 'unknown',
499             'options' => {},
500             };
501             }
502              
503 5 50       31 return wantarray ? %$info : $info;
504             }
505              
506              
507              
508              
509             =item password_hash($password, $algo, %options)
510              
511             Similar to L with the difference that the $algo argument is optional and defaults to PASSWORD_DEFAULT for your programming pleasure.
512              
513             Important notes about the 'salt' option which you shouldn't use in the first place:
514              
515             - The PASSWORD_BCRYPT 'salt' option is deprecated since PHP 7.0, but if you do pass it, then it must be 16 bytes long!
516             - 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!
517              
518             Returns a string.
519              
520             =cut
521              
522             sub password_hash {
523 0 0 0 0 1 0 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
524 0         0 my $password = shift;
525 0   0     0 my $algo = shift // PASSWORD_DEFAULT;
526 0 0 0     0 my %options = @_ && ref($_[0]) ? %{$_[0]} : @_;
  0         0  
527 0         0 $options{'algo'} = $algo;
528 0         0 return $proto->_hash($password, %options);
529             }
530              
531              
532              
533              
534              
535             =item password_needs_rehash($crypted, $algo, %options)
536              
537             The same as L.
538              
539             =cut
540              
541             sub password_needs_rehash {
542 12 50 33 12 1 10899 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
543 12         42 return $proto->_needs_rehash(@_);
544             }
545              
546              
547              
548              
549              
550             =item password_verify($password, $crypted)
551              
552             The same as L.
553              
554             =cut
555              
556             sub password_verify {
557 25 50 33 25 1 18039 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
558 25   100     1046 return $proto->_verify(@_) // 0;
559             }
560              
561              
562              
563              
564              
565             =back
566              
567             =head1 STATIC METHODS
568              
569             =over
570              
571             =item algos()
572              
573             See C.
574              
575             =cut
576              
577             sub algos {
578 1 50 33 1 1 22 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
579 1         3 return $proto->_algos(@_);
580             }
581              
582              
583              
584              
585             =item get_info($crypted)
586              
587             Similar to C, with the difference that this returns undef if the $crypted string format is unrecognized.
588             Returns a hashref if there is a match, else undef.
589              
590             =cut
591              
592             sub get_info {
593 5 50 33 5 1 6185 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
594 5         23 my $info = $proto->_get_info(@_); # hashref or undef
595 5 50       58 return wantarray ? %$info : $info;
596             }
597              
598              
599              
600              
601             =item hash($password, %options)
602              
603             Similar to C but with a different argument signature.
604             The difference is that this method doesn't have an $algo argument, but instead allows the algorithm to be specified using the 'algo' option (in %options).
605              
606             =cut
607              
608             sub hash {
609 9 50 33 9 1 118 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
610 9         24 my $password = shift;
611 9 50 33     72 my %options = @_ && ref($_[0]) ? %{$_[0]} : @_;
  0         0  
612 9         50 return $proto->_hash($password, %options);
613             }
614              
615              
616              
617              
618             =item needs_rehash($crypted, $algo, %options)
619              
620             See C.
621              
622             =cut
623              
624             sub needs_rehash {
625 12 50 33 12 1 13001 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
626 12         46 return $proto->_needs_rehash(@_);
627             }
628              
629              
630              
631              
632             =item verify($password, $crypted)
633              
634             See C.
635              
636             =cut
637              
638             sub verify {
639 25 50 33 25 1 356045 my $proto = @_ && UNIVERSAL::isa($_[0], __PACKAGE__) ? shift : __PACKAGE__;
640 25         132 return $proto->_verify(@_);
641             }
642              
643              
644              
645              
646              
647             =back
648              
649             =cut
650              
651             1;
652              
653             __END__