File Coverage

blib/lib/Crypt/Passphrase/Scrypt.pm
Criterion Covered Total %
statement 60 63 95.2
branch 8 12 66.6
condition 13 19 68.4
subroutine 13 14 92.8
pod 5 6 83.3
total 99 114 86.8


line stmt bran cond sub pod time code
1             package Crypt::Passphrase::Scrypt;
2             $Crypt::Passphrase::Scrypt::VERSION = '0.004';
3 1     1   70063 use strict;
  1         9  
  1         29  
4 1     1   5 use warnings;
  1         2  
  1         36  
5              
6 1     1   446 use parent 'Crypt::Passphrase::Encoder';
  1         277  
  1         5  
7              
8 1     1   7273 use Carp 'croak';
  1         2  
  1         47  
9 1     1   5 use Crypt::ScryptKDF qw/scrypt_b64 scrypt_raw/;
  1         2  
  1         65  
10 1     1   6 use MIME::Base64 qw/encode_base64 decode_base64/;
  1         2  
  1         970  
11             our @CARP_NOT = 'Crypt::Passphrase';
12              
13             sub new {
14 3     3 1 93 my ($class, %args) = @_;
15             return bless {
16             cost => $args{cost} || 16,
17             block_size => $args{block_size} || 8,
18             parallel => $args{parallel} || 1,
19             salt_size => $args{salt_size} || 16,
20 3   100     58 output_size => $args{output_size} || 32,
      100        
      50        
      100        
      50        
21             }, $class;
22             }
23              
24             sub hash_password {
25 2     2 1 12 my ($self, $password) = @_;
26 2         16 my $salt = $self->random_bytes($self->{salt_size});
27 2         10283 my $hash = scrypt_b64($password, $salt, 1 << $self->{cost}, $self->{block_size}, $self->{parallel}, $self->{output_size});
28 2         719832 return sprintf '$scrypt$ln=%d,r=%d,p=%d$%s$%s', $self->{cost}, $self->{block_size}, $self->{parallel}, encode_base64($salt), $hash;
29             }
30              
31             my $decode_regex = qr/ \A \$ scrypt \$ ln=(\d+),r=(\d+),p=(\d+) \$ ([^\$]+) \$ ([^\$]*) \z /x;
32              
33             sub needs_rehash {
34 5     5 1 3271 my ($self, $hash) = @_;
35 5 100       170 my ($cost, $block_size, $parallel, $salt64, $hash64) = $hash =~ $decode_regex or return 1;
36 4 50 66     59 return !!1 if $cost != $self->{cost} or $block_size != $self->{block_size} or $parallel != $self->{parallel};
      66        
37 3 50 33     36 return !!1 if length decode_base64($salt64) != $self->{salt_size} or length decode_base64($hash64) != $self->{output_size};
38 3         18 return !!0;
39             }
40              
41             sub crypt_subtypes {
42 0     0 1 0 return ('scrypt', '7');
43             }
44              
45             my $base64_digits = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
46             sub _decode_crypt64 {
47 2     2   5 my $digits = shift;
48 2         5 my $ndigits = length($digits);
49 2         7 my $npadbytes = 3 - ($ndigits + 3) % 4;
50 2         7 $digits .= "." x $npadbytes;
51 2         4 my $bytes = "";
52 2         9 for(my $i = 0; $i < $ndigits; $i += 4) {
53 22         58 my $v = index($base64_digits, substr $digits, $i, 1) |
54             (index($base64_digits, substr $digits, $i + 1, 1) << 6) |
55             (index($base64_digits, substr $digits, $i + 2, 1) << 12) |
56             (index($base64_digits, substr $digits, $i + 3, 1) << 18);
57 22         64 $bytes .= chr($v & 0xff) . chr(($v >> 8) & 0xff) . chr(($v >> 16) & 0xff);
58             }
59 2         8 substr $bytes, -$npadbytes, $npadbytes, "";
60 2         9 return $bytes;
61             }
62              
63             sub _decode_number {
64 6     6   12 my $input = shift;
65 6         11 my $result = 0;
66 6         17 for (0 .. length($input) - 1) {
67 22         48 $result += index($base64_digits, substr $input, $_, 1) * (1 << (6 * $_));
68             }
69 6         19 return $result;
70             }
71              
72             my $char64 = qr{[./0-9A-Za-z]};
73             my $regex7 = qr/ ^ \$7\$ ($char64) ($char64{5}) ($char64{5}) ([^\$]{22}) \$ ([^\$]*) /x;
74              
75             sub verify_password {
76 5     5 1 827 my ($class, $password, $hash) = @_;
77 5 100       134 if (my ($cost, $block_size, $parallel, $salt64, $hash64) = $hash =~ $decode_regex) {
    50          
78 4         27 my $old_hash = decode_base64($hash64);
79 4         73 my $new_hash = scrypt_raw($password, decode_base64($salt64), 1 << $cost, $block_size, $parallel, length $old_hash);
80 4         2107859 return $class->secure_compare($new_hash, $old_hash);
81             }
82             elsif (my ($encoded_cost, $encoded_block_size, $encoded_parallel, $salt, $encoded_hash) = $hash =~ $regex7) {
83 1         5 my ($cost, $block_size, $parallel) = map { _decode_number($_) } $encoded_cost, $encoded_block_size, $encoded_parallel;
  3         7  
84 1         5 my $old_hash = _decode_crypt64($encoded_hash);
85 1         7 my $new_hash = scrypt_raw($password, $salt, 1 << $cost, $block_size, $parallel, length $old_hash);
86 1         943918 return $class->secure_compare($new_hash, $old_hash);
87             }
88 0         0 return !!0;
89             }
90              
91             sub recode_hash {
92 1     1 0 4 my ($self, $hash) = @_;
93 1 50       16 if (my ($encoded_cost, $encoded_block_size, $encoded_parallel, $salt, $encoded_hash) = $hash =~ $regex7) {
94 1         5 my ($cost, $block_size, $parallel) = map { _decode_number($_) } $encoded_cost, $encoded_block_size, $encoded_parallel;
  3         7  
95 1         6 my $recoded_hash = encode_base64(_decode_crypt64($encoded_hash));
96 1         16 return sprintf '$scrypt$ln=%d,r=%d,p=%d$%s$%s', $cost, $block_size, $parallel, encode_base64($salt), $recoded_hash;
97             }
98 0           return $hash;
99             }
100              
101             1;
102              
103             #ABSTRACT: A scrypt encoder for Crypt::Passphrase
104              
105             __END__