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