File Coverage

blib/lib/PHP/Functions/Password.pm
Criterion Covered Total %
statement 220 249 88.3
branch 77 126 61.1
condition 54 110 49.0
subroutine 39 39 100.0
pod 10 10 100.0
total 400 534 74.9


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