File Coverage

blib/lib/Crypt/Passphrase/Bcrypt/Encrypted.pm
Criterion Covered Total %
statement 20 81 24.6
branch 0 18 0.0
condition 0 11 0.0
subroutine 7 17 41.1
pod 5 6 83.3
total 32 133 24.0


line stmt bran cond sub pod time code
1             package Crypt::Passphrase::Bcrypt::Encrypted;
2             $Crypt::Passphrase::Bcrypt::Encrypted::VERSION = '0.009';
3 1     1   158636 use 5.014;
  1         3  
4 1     1   4 use warnings;
  1         2  
  1         65  
5              
6 1     1   380 use Crypt::Passphrase 0.019 -encoder;
  1         3755  
  1         5  
7 1     1   3218 use Crypt::Passphrase::Bcrypt;
  1         4  
  1         72  
8              
9 1     1   10 use Carp 'croak';
  1         3  
  1         71  
10 1     1   6 use Crypt::Bcrypt 0.011 qw/bcrypt_prehashed bcrypt_check_prehashed bcrypt_supported_prehashes/;
  1         16  
  1         64  
11 1     1   6 use MIME::Base64 qw/encode_base64 decode_base64/;
  1         2  
  1         1172  
12              
13             sub new {
14 0     0 0   my ($class, %args) = @_;
15 0   0       $args{hash} //= 'sha384';
16 0           my $self = Crypt::Passphrase::Bcrypt->new(%args);
17 0           $self->{cipher} = $args{cipher};
18 0           $self->{active} = $args{active};
19 0           return bless $self, $class;
20             }
21              
22             my $format = '$bcrypt-%s-encrypted-%s$t=%s,r=%d,keyid=%s$%s$%s';
23              
24             sub _pack_hash {
25 0     0     my ($hash_alg, $cipher, $subtype, $id, $cost, $salt, $hash) = @_;
26 0           my $encoded_salt = encode_base64($salt, '') =~ tr/=//dr;
27 0           my $encoded_hash = encode_base64($hash, '') =~ tr/=//dr;
28 0           return sprintf $format, $hash_alg, $cipher, $subtype, $cost, $id, $encoded_salt, $encoded_hash;
29             }
30              
31             my $regex = qr/ ^ \$ bcrypt-(sha\d{3})-encrypted-([^\$]+) \$ t=(\w+), r=(\d+), keyid=([^\$,]+) \$ ([^\$]+) \$ (.*) $ /x;
32              
33             sub _unpack_hash {
34 0     0     my $pwhash = shift;
35 0 0         my ($hash_type, $alg, $subtype, $cost, $id, $encoded_salt, $encoded_hash) = $pwhash =~ $regex or return;
36 0           my $salt = decode_base64($encoded_salt);
37 0           my $hash = decode_base64($encoded_hash);
38 0           return ($hash_type, $alg, $subtype, $id, $cost, $salt, $hash);
39             }
40              
41             my $unencrypted_format = '$bcrypt-%s$v=2,t=%s,r=%d$%s$%s';
42              
43             sub _pack_raw {
44 0     0     my ($hash_type, $subtype, $cost, $salt, $hash) = @_;
45 0           my $encoded_salt = encode_base64($salt, '') =~ tr{A-Za-z0-9+/=}{./A-Za-z0-9}dr;
46 0           my $encoded_hash = encode_base64($hash, '') =~ tr{A-Za-z0-9+/=}{./A-Za-z0-9}dr;
47 0           return sprintf $unencrypted_format, $hash_type, $subtype, $cost, $encoded_salt, $encoded_hash;
48             }
49              
50             my $unencrypted_regex = qr/ ^ \$ bcrypt-(sha\d{3}) \$ v=2, t=(\w+), r=(\d+)\$ ([^\$]+) \$ (.*) $ /x;
51              
52             sub _unpack_raw {
53 0     0     my $input = shift;
54 0 0         my ($hash_type, $subtype, $cost, $encoded_salt, $encoded_hash) = $input =~ $unencrypted_regex or return;
55 0           my $salt = decode_base64($encoded_salt =~ tr{./A-Za-z0-9}{A-Za-z0-9+/}r);
56 0           my $hash = decode_base64($encoded_hash =~ tr{./A-Za-z0-9}{A-Za-z0-9+/}r);
57 0           return ($hash_type, $subtype, $cost, $salt, $hash);
58             }
59              
60             sub recode_hash {
61 0     0 1   my ($self, $input, $to) = @_;
62 0   0       $to //= $self->{active};
63 0 0         if (my ($hash_type, $alg, $subtype, $id, $cost, $salt, $hash) = _unpack_hash($input)) {
    0          
64 0 0 0       return $input if $id eq $to and $alg eq $self->{cipher};
65 0   0       return eval {
66 0           my $decrypted = $self->decrypt_hash($alg, $id, $salt, $hash);
67 0           my $encrypted = $self->encrypt_hash($self->{cipher}, $to, $salt, $decrypted);
68 0           _pack_hash($hash_type, $self->{cipher}, $subtype, $to, $cost, $salt, $encrypted);
69             } // $input;
70             }
71             elsif (($hash_type, $subtype, $cost, $salt, $hash) = _unpack_raw) {
72 0           my $encrypted = $self->encrypt_hash($self->{cipher}, $to, $salt, $hash);
73 0           return _pack_hash($hash_type, $self->{cipher}, $subtype, $to, $cost, $salt, $encrypted);
74             }
75             else {
76 0           return $input;
77             }
78             }
79              
80             sub hash_password {
81 0     0 1   my ($self, $password) = @_;
82              
83 0           my $salt = $self->random_bytes(16);
84 0           my $raw = bcrypt_prehashed($password, $self->{subtype}, $self->{cost}, $salt, $self->{hash});
85 0           my ($hash_type, $subtype, $cost, $salt2, $hash) = _unpack_raw($raw);
86              
87 0           my $encrypted = $self->encrypt_hash($self->{cipher}, $self->{active}, $salt, $hash);
88              
89 0           return _pack_hash(@{$self}{qw/hash cipher subtype active cost/}, $salt2, $encrypted);
  0            
90             }
91              
92             sub needs_rehash {
93 0     0 1   my ($self, $pwhash) = @_;
94 0 0         my ($hash_type, $alg, $subtype, $id, $cost, $salt, $encrypted_hash) = _unpack_hash($pwhash) or return 1;
95 0           return $pwhash ne _pack_hash(@{$self}{qw/hash cipher subtype active cost/}, $salt, $encrypted_hash);
  0            
96             }
97              
98             sub crypt_subtypes {
99 0     0 1   my $self = shift;
100 0           my @result;
101 0           my @supported = $self->supported_ciphers;
102 0           for my $hash_alg (bcrypt_supported_prehashes) {
103 0           push @result, "bcrypt-$hash_alg", map { "bcrypt-$hash_alg-encrypted-$_" } @supported
  0            
104             }
105 0           return @result;
106             }
107              
108             sub verify_password {
109 0     0 1   my ($self, $password, $pwhash) = @_;
110 0 0         if (my ($hash_type, $alg, $subtype, $id, $cost, $salt, $encrypted_hash) = _unpack_hash($pwhash)) {
    0          
111 0 0         my $hash = eval { $self->decrypt_hash($alg, $id, $salt, $encrypted_hash) } or return !!0;
  0            
112 0           my $primary = _pack_raw($hash_type, $subtype, $cost, $salt, $hash);
113 0           return bcrypt_check_prehashed($password, $primary);
114             }
115             elsif ($pwhash =~ $unencrypted_regex) {
116 0           return bcrypt_check_prehashed($password, $pwhash);
117             }
118             else {
119 0           return !!0;
120             }
121             }
122              
123             1;
124              
125             #ABSTRACT: A base-class for encrypting/peppered Argon2 encoders for Crypt::Passphrase
126              
127             __END__