File Coverage

blib/lib/Dancer2/Plugin/Passphrase/Core.pm
Criterion Covered Total %
statement 97 99 97.9
branch 24 30 80.0
condition 8 12 66.6
subroutine 15 15 100.0
pod 0 4 0.0
total 144 160 90.0


line stmt bran cond sub pod time code
1             use strict;
2 10     10   50 use warnings;
  10         17  
  10         205  
3 10     10   38 use Carp qw(croak);
  10         18  
  10         191  
4 10     10   39 use Digest;
  10         16  
  10         428  
5 10     10   4032 use MIME::Base64 qw(decode_base64 encode_base64);
  10         4373  
  10         270  
6 10     10   3266 use Data::Entropy::Algorithms qw(rand_bits rand_int);
  10         5217  
  10         537  
7 10     10   3571  
  10         110578  
  10         10410  
8             # ABSTRACT: Passphrases and Passwords as objects for Dancer2
9              
10             =head1 NAME
11              
12             Dancer2::Plugin::Passphrase::Core - Core package for Dancer2::Plugin::Passphrase.
13              
14             =head1 DESCRIPTION
15              
16             B<FOR INTERNAL USE ONLY>
17              
18             =head1 AUTHOR
19              
20             Maintainer: Henk van Oers <hvoers@cpan.org>
21              
22             =head1 COPYRIGHT AND LICENSE
23              
24             This software is copyright (c) 2012 by James Aitken.
25              
26             This is free software; you can redistribute it and/or modify it under
27             the same terms as the Perl 5 programming language system itself.
28              
29             =cut
30              
31             my $class = shift;
32             my @args = @_;
33 98     98 0 2193 return bless { @args == 1 ? %{$args[0]} : @args }, $class;
34 98         233 }
35 98 50       627  
  0         0  
36             # { algorithm => '...', this => '...' }
37             my $self = shift;
38             my $options = shift;
39             my $algorithm = $self->{'algorithm'};
40 26     26   41 my $settings = {};
41 26         36  
42 26         52 # if we got options
43 26         37 if ($options) {
44             $algorithm = delete $options->{'algorithm'};
45             $settings =
46 26 100       61 defined $options->{$algorithm}
47 24         42 ? $options->{$algorithm}
48             : $self->{$algorithm};
49             }
50              
51 24 50       66 # Specify empty string to get an unsalted hash
52             # Leaving it undefs results in 128 random bits being used as salt
53             # bcrypt requires this amount, and is reasonable for other algorithms
54             $settings->{'salt'} = rand_bits(128)
55             unless defined $settings->{'salt'};
56              
57             # RFC 2307 scheme is based on the algorithm, with a prefixed 'S' for salted
58 26 50       108 $settings->{'scheme'} = join '', $algorithm =~ /[\w]+/g;
59             $settings->{'scheme'} = 'S'. $settings->{'scheme'}
60             if $settings->{'salt'};
61 26         28219  
62             if ( $settings->{'scheme'} eq 'SHA1' ) {
63 26 50       93 $settings->{'scheme'} = 'SHA';
64             } elsif ( $settings->{'scheme'} eq 'SSHA1' ) {
65 26 50       90 $settings->{'scheme'} = 'SSHA';
    100          
66 0         0 }
67              
68 6         15 # Bcrypt requires a cost parameter
69             if ( $algorithm eq 'Bcrypt' ) {
70             $settings->{'scheme'} = 'CRYPT';
71             $settings->{'type'} = '2a';
72 26 100       53  
73 5         12 $settings->{'cost'} //= $self->{'Bcrypt'}{'cost'} || 4;
74 5         9 $settings->{'cost'} = 31 if $settings->{'cost'} > 31;
75             $settings->{'cost'} = sprintf '%02d', $settings->{'cost'};
76 5   50     46 }
      33        
77 5 50       14  
78 5         22 $settings->{'algorithm'} = $algorithm;
79             $settings->{'plaintext'} = $self->{'plaintext'};
80              
81 26         40 return $settings;
82 26         46 }
83              
84 26         47 # From Crypt::Eksblowfish::Bcrypt.
85             # Bcrypt uses it's own variation on base64
86             my ($octets) = @_;
87             my $text = encode_base64($octets, '');
88             $text =~ tr{A-Za-z0-9+/=}{./A-Za-z0-9}d;
89             return $text;
90 84     84   160 }
91 84         201  
92 84         143  
93 84         189 # And the decoder of bcrypt's custom base64
94             my ($text) = @_;
95             $text =~ tr{./A-Za-z0-9}{A-Za-z0-9+/};
96             $text .= "=" x (3 - (length($text) + 3) % 4);
97             return decode_base64($text);
98             }
99 38     38   110  
100 38         92 # Extracts the settings from an RFC 2307 string
101 38         171 my ($self, $rfc2307_string) = @_;
102 38         161 my $settings = {};
103              
104             my ($scheme, $rfc_settings) = ($rfc2307_string =~ m/^{(\w+)}(.*)/s);
105              
106             unless ($scheme && $rfc_settings) {
107 69     69   146 croak "An RFC 2307 compliant string must be passed to matches()";
108 69         111 }
109              
110 69         331 if ($scheme eq 'CRYPT') {
111             if ($rfc_settings =~ m/^\$2(?:a|x|y)\$/) {
112 69 100 66     365 $scheme = 'Bcrypt';
113 1         142 $rfc_settings =~ m{\A\$(2a|2x|2y)\$([0-9]{2})\$([./A-Za-z0-9]{22})}x;
114              
115             @{$settings}{qw<type cost salt>} = ( $1, $2, _de_bcrypt_base64($3) );
116 68 100       202 } else {
117 39 100       177 croak "Unknown CRYPT format";
118 38         75 }
119 38         149 }
120              
121 38         160 my $scheme_meta = {
  38         173  
122             'MD5' => { algorithm => 'MD5', octets => 128 / 8 },
123 1         58 'SMD5' => { algorithm => 'MD5', octets => 128 / 8 },
124             'SHA' => { algorithm => 'SHA-1', octets => 160 / 8 },
125             'SSHA' => { algorithm => 'SHA-1', octets => 160 / 8 },
126             'SHA224' => { algorithm => 'SHA-224', octets => 224 / 8 },
127 67         1288 'SSHA224' => { algorithm => 'SHA-224', octets => 224 / 8 },
128             'SHA256' => { algorithm => 'SHA-256', octets => 256 / 8 },
129             'SSHA256' => { algorithm => 'SHA-256', octets => 256 / 8 },
130             'SHA384' => { algorithm => 'SHA-384', octets => 384 / 8 },
131             'SSHA384' => { algorithm => 'SHA-384', octets => 384 / 8 },
132             'SHA512' => { algorithm => 'SHA-512', octets => 512 / 8 },
133             'SSHA512' => { algorithm => 'SHA-512', octets => 512 / 8 },
134             'Bcrypt' => { algorithm => 'Bcrypt', octets => 128 / 8 },
135             };
136              
137             $settings->{'scheme'} = $scheme;
138             $settings->{'algorithm'} = $scheme_meta->{$scheme}{algorithm};
139             $settings->{'plaintext'} = $self->{'plaintext'};;
140              
141             if ( !defined $settings->{'salt'} ) {
142             $settings->{'salt'} = substr(
143 67         163 decode_base64($rfc_settings),
144 67         144 $scheme_meta->{$scheme}{octets},
145 67         129 );
146             }
147 67 100       181  
148             return $settings;
149             }
150              
151 29         112 my ( $self, $settings ) = @_;
152             my $hasher = Digest->new( $settings->{'algorithm'} );
153             my ( $hash, $rfc2307 );
154 67         305  
155             if ( $settings->{'algorithm'} eq 'Bcrypt' ) {
156             $hasher->add( $settings->{'plaintext'} );
157             $hasher->salt( $settings->{'salt'} );
158 93     93   172 $hasher->cost( $settings->{'cost'} );
159 93         400  
160 93         25772 $hash = $hasher->digest;
161             $rfc2307 = '{CRYPT}$'
162 93 100       247 . $settings->{'type'} . '$'
163 43         167 . $settings->{'cost'} . '$'
164 43         374 . _en_bcrypt_base64( $settings->{'salt'} )
165 43         4243 . _en_bcrypt_base64($hash);
166             } else {
167 43         937 $hasher->add( $settings->{'plaintext'} );
168             $hasher->add( $settings->{'salt'} );
169              
170             $hash = $hasher->digest;
171 42         61407 $rfc2307 = '{' . $settings->{'scheme'} . '}'
172             . encode_base64(
173             $hash . $settings->{'salt'},
174 50         164 ''
175 47         92 );
176             }
177 47         182  
178             return Dancer2::Plugin::Passphrase::Hashed->new(
179             hash => $hash,
180 47         204 rfc2307 => $rfc2307,
181             %{$settings},
182             );
183             }
184              
185             my $self = shift;
186             my $options = shift;
187             my $settings = $self->_merge_options($options);
188 89         180  
  89         521  
189             return $self->_calculate_hash($settings);
190             }
191              
192             my ($self, $options) = @_;
193 26     26 0 46  
194 26         34 # Default is 16 URL-safe base64 chars. Supported everywhere and a reasonable length
195 26         53 my $length = $options->{length} || 16;
196             my $charset = $options->{charset} || ['a'..'z', 'A'..'Z', '0'..'9', '-', '_'];
197 26         64  
198             return join '', map { @$charset[rand_int scalar @$charset] } 1..$length;
199             }
200              
201 3     3 0 7 my ($self, $stored_hash) = @_;
202              
203             my $settings = $self->_extract_settings($stored_hash);
204 3   100     9 my $new_hash = $self->_calculate_hash($settings)->rfc2307;
205 3   100     23  
206             return ($new_hash eq $stored_hash) ? 1 : undef;
207 3         7 }
  51         6961  
208              
209             1;