File Coverage

blib/lib/Dancer2/Plugin/Passphrase/Core.pm
Criterion Covered Total %
statement 97 99 97.9
branch 25 32 78.1
condition 6 7 85.7
subroutine 15 15 100.0
pod 0 4 0.0
total 143 157 91.0


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::Passphrase::Core;
2 11     11   68 use strict;
  11         30  
  11         289  
3 11     11   54 use warnings;
  11         20  
  11         291  
4 11     11   58 use Carp qw(croak);
  11         21  
  11         581  
5 11     11   5231 use Digest;
  11         5835  
  11         351  
6 11     11   4702 use MIME::Base64 qw(decode_base64 encode_base64);
  11         6752  
  11         725  
7 11     11   5072 use Data::Entropy::Algorithms qw(rand_bits rand_int);
  11         145868  
  11         14209  
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 2481 my $class = shift;
34 98         242 my @args = @_;
35 98 50       657 return bless { @args == 1 ? %{$args[0]} : @args }, $class;
  0         0  
36             }
37              
38             # { algorithm => '...', this => '...' }
39             sub _merge_options {
40 26     26   46 my $self = shift;
41 26         41 my $options = shift;
42 26         64 my $algorithm = $self->{'algorithm'};
43 26         52 my $settings = {};
44              
45             # if we got options
46 26 100       74 if ($options) {
47 24         55 $algorithm = delete $options->{'algorithm'};
48             $settings =
49             defined $options->{$algorithm}
50             ? $options->{$algorithm}
51 24 50       75 : $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       130 unless defined $settings->{'salt'};
59              
60             # RFC 2307 scheme is based on the algorithm, with a prefixed 'S' for salted
61 26         38900 $settings->{'scheme'} = join '', $algorithm =~ /[\w]+/g;
62             $settings->{'scheme'} = 'S'. $settings->{'scheme'}
63 26 50       115 if $settings->{'salt'};
64              
65 26 50       121 if ( $settings->{'scheme'} eq 'SHA1' ) {
    100          
66 0         0 $settings->{'scheme'} = 'SHA';
67             } elsif ( $settings->{'scheme'} eq 'SSHA1' ) {
68 6         18 $settings->{'scheme'} = 'SSHA';
69             }
70              
71             # Bcrypt requires a cost parameter
72 26 100       77 if ( $algorithm eq 'Bcrypt' ) {
73 5         15 $settings->{'scheme'} = 'CRYPT';
74 5         12 $settings->{'type'} = '2a';
75             $settings->{'cost'} =
76 5 50       22 defined $settings->{'cost'} ? $settings->{'cost'} : 4;
77 5 50       19 $settings->{'cost'} = 31 if $settings->{'cost'} > 31;
78 5         29 $settings->{'cost'} = sprintf '%02d', $settings->{'cost'};
79             }
80              
81 26         55 $settings->{'algorithm'} = $algorithm;
82 26         53 $settings->{'plaintext'} = $self->{'plaintext'};
83              
84 26         57 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   157 my ($octets) = @_;
91 84         214 my $text = encode_base64($octets, '');
92 84         152 $text =~ tr{A-Za-z0-9+/=}{./A-Za-z0-9}d;
93 84         175 return $text;
94             }
95              
96              
97             # And the decoder of bcrypt's custom base64
98             sub _de_bcrypt_base64 {
99 38     38   93 my ($text) = @_;
100 38         76 $text =~ tr{./A-Za-z0-9}{A-Za-z0-9+/};
101 38         123 $text .= "=" x (3 - (length($text) + 3) % 4);
102 38         130 return decode_base64($text);
103             }
104              
105             # Extracts the settings from an RFC 2307 string
106             sub _extract_settings {
107 69     69   118 my ($self, $rfc2307_string) = @_;
108 69         112 my $settings = {};
109              
110 69         367 my ($scheme, $rfc_settings) = ($rfc2307_string =~ m/^{(\w+)}(.*)/s);
111              
112 69 100 66     327 unless ($scheme && $rfc_settings) {
113 1         119 croak "An RFC 2307 compliant string must be passed to matches()";
114             }
115              
116 68 100       171 if ($scheme eq 'CRYPT') {
117 39 100       174 if ($rfc_settings =~ m/^\$2(?:a|x|y)\$/) {
118 38         66 $scheme = 'Bcrypt';
119 38         113 $rfc_settings =~ m{\A\$(2a|2x|2y)\$([0-9]{2})\$([./A-Za-z0-9]{22})}x;
120              
121 38         91 @{$settings}{qw} = ( $1, $2, _de_bcrypt_base64($3) );
  38         152  
122             } else {
123 1         68 croak "Unknown CRYPT format";
124             }
125             }
126              
127 67         1026 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         178 $settings->{'scheme'} = $scheme;
144 67         129 $settings->{'algorithm'} = $scheme_meta->{$scheme}{algorithm};
145 67         121 $settings->{'plaintext'} = $self->{'plaintext'};;
146              
147 67 100       161 if ( !defined $settings->{'salt'} ) {
148             $settings->{'salt'} = substr(
149             decode_base64($rfc_settings),
150             $scheme_meta->{$scheme}{octets},
151 29         143 );
152             }
153              
154 67         360 return $settings;
155             }
156              
157             sub _calculate_hash {
158 93     93   209 my ( $self, $settings ) = @_;
159 93         329 my $hasher = Digest->new( $settings->{'algorithm'} );
160 93         41593 my ( $hash, $rfc2307 );
161              
162 93 100       226 if ( $settings->{'algorithm'} eq 'Bcrypt' ) {
163 43         134 $hasher->add( $settings->{'plaintext'} );
164 43         352 $hasher->salt( $settings->{'salt'} );
165 43         5008 $hasher->cost( $settings->{'cost'} );
166              
167 43         873 $hash = $hasher->digest;
168             $rfc2307 = '{CRYPT}$'
169             . $settings->{'type'} . '$'
170             . $settings->{'cost'} . '$'
171 42         80419 . _en_bcrypt_base64( $settings->{'salt'} )
172             . _en_bcrypt_base64($hash);
173             } else {
174 50         211 $hasher->add( $settings->{'plaintext'} );
175 47         128 $hasher->add( $settings->{'salt'} );
176              
177 47         246 $hash = $hasher->digest;
178             $rfc2307 = '{' . $settings->{'scheme'} . '}'
179             . encode_base64(
180 47         244 $hash . $settings->{'salt'},
181             ''
182             );
183             }
184              
185             return Dancer2::Plugin::Passphrase::Hashed->new(
186             hash => $hash,
187             rfc2307 => $rfc2307,
188 89         178 %{$settings},
  89         469  
189             );
190             }
191              
192             sub generate {
193 26     26 0 51 my $self = shift;
194 26         41 my $options = shift;
195 26         67 my $settings = $self->_merge_options($options);
196              
197 26         80 return $self->_calculate_hash($settings);
198             }
199              
200             sub generate_random {
201 3     3 0 8 my ($self, $options) = @_;
202              
203             # Default is 16 URL-safe base64 chars. Supported everywhere and a reasonable length
204 3   100     11 my $length = $options->{length} || 16;
205 3   100     26 my $charset = $options->{charset} || ['a'..'z', 'A'..'Z', '0'..'9', '-', '_'];
206              
207 3         9 return join '', map { @$charset[rand_int scalar @$charset] } 1..$length;
  51         7668  
208             }
209              
210             sub matches {
211 69     69 0 148 my ($self, $stored_hash) = @_;
212              
213 69         176 my $settings = $self->_extract_settings($stored_hash);
214 67         158 my $new_hash = $self->_calculate_hash($settings)->rfc2307;
215              
216 67 100       484 return ($new_hash eq $stored_hash) ? 1 : undef;
217             }
218              
219             1;