File Coverage

blib/lib/Passwords.pm
Criterion Covered Total %
statement 74 97 76.2
branch 21 38 55.2
condition 4 12 33.3
subroutine 15 15 100.0
pod 4 4 100.0
total 118 166 71.0


line stmt bran cond sub pod time code
1             package Passwords;
2              
3 6     6   27040 use 5.010;
  6         21  
  6         246  
4 6     6   41 use strict;
  6         11  
  6         185  
5 6     6   30 use warnings;
  6         44  
  6         146  
6 6     6   5338 use autodie;
  6         124334  
  6         37  
7 6     6   52183 use utf8;
  6         64  
  6         33  
8 6     6   203 use Carp;
  6         12  
  6         488  
9 6     6   5108 use Crypt::Eksblowfish::Bcrypt qw(bcrypt_hash en_base64 de_base64);
  6         39535  
  6         622  
10 6     6   5814 use Data::Entropy::Algorithms qw(rand_bits);
  6         95951  
  6         929  
11              
12             =encoding utf8
13              
14             =head1 NAME
15              
16             Passwords - Provides an easy to use API for the creation and management of passwords in a secure manner
17              
18             =head1 VERSION
19              
20             Version 0.01
21              
22             =cut
23              
24             our $VERSION = '0.01';
25              
26              
27             =head1 SYNOPSIS
28              
29             use Passwords;
30              
31             # create password-hash (simple way with bcrypt, random-salt, cost of 14)
32             my $hash = password_hash('perlhipster');
33             say $hash;
34            
35             # custom options
36             # my $hash = password_hash('perlhipster', PASSWORD_BCRYPT, ( 'cost' => 20, 'salt' => 'This-Is-ASillySalt2014'));
37            
38             # verify password
39             if (password_verify('perlhipster', $hash)) {
40             say 'ok';
41             } else {
42             say 'nok';
43             }
44              
45             =head1 EXPORT
46              
47             PASSWORD_DEFAULT
48             PASSWORD_BCRYPT
49            
50             password_get_info
51             password_hash
52             password_needs_rehash
53             password_verify
54              
55             =cut
56              
57             require Exporter;
58             our @ISA = 'Exporter';
59             our @EXPORT = qw(
60             PASSWORD_DEFAULT
61             PASSWORD_BCRYPT
62             password_get_info
63             password_hash
64             password_needs_rehash
65             password_verify
66             );
67              
68 6     6   128 use constant PASSWORD_DEFAULT => 1;
  6         12  
  6         344  
69 6     6   89 use constant PASSWORD_BCRYPT => 1;
  6         11  
  6         7174  
70              
71             =head1 SUBROUTINES
72              
73             =head2 password_get_info( $hash )
74              
75             Returns information about the given hash
76              
77             {
78             'algoName' => 'bcrypt',
79             'algo' => 1,
80             'options' => {
81             'cost' => 14
82             }
83             };
84              
85             =cut
86              
87             sub password_get_info {
88 4     4 1 654 my $hash = shift;
89 4         21 my %return = (
90             'algo' => 0,
91             'algoName' => 'unknown',
92             'options' => undef,
93             );
94            
95 4 50       13 if (not defined $hash) {
96 0         0 carp 'password_get_info(): Hash must be supplied';
97 0         0 return;
98             }
99            
100 4 50       13 if (_is_bcrypt($hash)) {
101 4         9 $return{'algo'} = PASSWORD_BCRYPT;
102 4         7 $return{'algoName'} = 'bcrypt';
103 4 50       32 $return{'options'}{'cost'} = $1 if $hash =~ m#\A\$2y\$([0-9]{2})\$#x;
104 4         15 $return{'options'}{'cost'} += 0;
105             }
106            
107 4         26 return %return;
108             }
109              
110             =head2 password_hash ( $password, $algo, %options )
111              
112             Creates a password hash
113              
114             Use the constant C for the current default algorithm which is
115             C.
116             Per default a cost of 14 will be used and a secure salt will be generated.
117              
118             =cut
119              
120             sub password_hash {
121 1     1 1 895 my $password = shift;
122 1         3 my $algo = shift;
123 1         7 my %options = @_;
124            
125 1 50       6 if (not defined $password) {
126 0         0 carp 'password_hash(): Password must be supplied';
127 0         0 return;
128             }
129            
130 1 50       5 if (not defined $algo) {
131 0         0 $algo = PASSWORD_DEFAULT;
132             }
133              
134             # parse options
135 1 50       6 if ($algo == PASSWORD_BCRYPT) {
136            
137             # get cost
138 1 50 33     22 if (not exists $options{'cost'} or $options{'cost'} =~ /\D/ or 0+$options{'cost'} < 4) {
      33        
139 0         0 $options{'cost'} = 14;
140             } else {
141 1         4 $options{'cost'} = 0+$options{'cost'};
142             }
143            
144             # get/generate salt
145 1 50       10 if (not exists $options{'salt'}) {
    50          
146 0         0 $options{'salt'} = rand_bits(128);
147             } elsif ($options{'salt'} !~ m#\A[\x00-\xff]{16}\z#) {
148 0         0 carp 'password_hash(): Provided salt needs to have 16 octets';
149 0         0 return;
150             }
151            
152             # hash password
153 1         12 $password = bcrypt_hash({
154             'key_nul' => 1,
155             'cost' => $options{'cost'},
156             'salt' => $options{'salt'},
157             }, $password);
158            
159 1         19017 return sprintf('$2y$%02d$%s%s', $options{'cost'}, en_base64($options{'salt'}), en_base64($password));
160            
161             } else {
162 0         0 carp sprintf('password_hash(): Unknown password hashing algorithm: %s', $algo);
163 0         0 return;
164             }
165             }
166              
167             =head2 password_needs_rehash ( $hash, $algo, %options )
168              
169             Checks if the given hash matches the given options
170              
171             =cut
172              
173             sub password_needs_rehash {
174 3     3 1 230 my $hash = shift;
175 3         7 my $algo = shift;
176 3         8 my %options = @_;
177            
178 3 50       10 if (not defined $hash) {
179 0         0 carp 'password_needs_rehash(): Hash must be supplied';
180 0         0 return;
181             }
182            
183 3 50 33     26 if (not defined $algo or $algo =~ /\D/) {
184 0         0 carp 'password_needs_rehash(): Algo must be supplied';
185 0         0 return;
186             }
187            
188 3         10 my %info = password_get_info($hash);
189 3 50       11 if ($info{'algo'} != $algo) {
190 0         0 return 1;
191             }
192            
193 3 50       11 if ($info{'algo'} == PASSWORD_BCRYPT) {
194 3         4 my $cost = 14;
195 3 100       10 if (exists $options{'cost'}) {
196 2         4 $cost = 0+$options{'cost'};
197             }
198            
199 3 100       10 if ($cost != $info{'options'}{'cost'}) {
200 2         41 return 1;
201             }
202             }
203            
204 1         8 return 0;
205             }
206              
207             =head2 password_verify ( $password, $hash )
208              
209             Verifies that a password matches a hash
210              
211             =cut
212              
213             sub password_verify {
214 3     3 1 1292 my ($password, $hash) = @_;
215            
216 3 50       21 if (not defined $password) {
    50          
217 0         0 carp 'password_verify(): Password must be supplied';
218 0         0 return;
219             } elsif (not defined $hash) {
220 0         0 carp 'password_verify(): Hash must be supplied';
221 0         0 return;
222             }
223            
224 3 50       11 if (not _is_bcrypt($hash)) {
225 0         0 carp 'password_verify(): Unsupported password hashing algorithm';
226 0         0 return;
227             }
228            
229 3         16 $hash =~ m#\A\$2y\$([0-9]{2})\$([./A-Za-z0-9]{22})([./A-Za-z0-9]{31})\z#x;
230 3         15 my ($p_cost, $p_salt_base64, $p_hash_base64) = ($1, $2, $3);
231            
232 3         15 $password = bcrypt_hash({
233             'key_nul' => 1,
234             'cost' => $p_cost,
235             'salt' => de_base64($p_salt_base64),
236             }, $password);
237            
238 3         261437 return $password eq de_base64($p_hash_base64);
239            
240             }
241              
242             sub _is_bcrypt {
243 7     7   15 my $hash = shift;
244 7 50 33     66 if (substr($hash, 0, 4) eq '$2y$' and length($hash) == 60) {
245 7         29 return 1;
246             }
247 0           return 0;
248             }
249              
250             =head1 AUTHOR
251              
252             Günter Grodotzki Eguenter@perlhipster.comE
253              
254             =head1 BUGS
255              
256             Please report any bugs or feature requests via L
257              
258             =head1 ACKNOWLEDGEMENTS
259              
260             This package is not a new invention, everything needed was already out there.
261             To avoid confusion, especially to newer developers this offers a dead simple
262             wrapper on a easy to remember namespace. Additionally this package is compatible
263             with PHP (and most likely other languages) which makes it a great addition on
264             multi-lang plattforms.
265              
266             Therefore props go to:
267              
268             =over 3
269              
270             =item * crypt_blowfish
271              
272             L
273              
274             =item * Andrew Main (ZEFRAM)
275              
276             L L
277              
278             =item * Anthony Ferrara (ircmaxell)
279              
280             L
281              
282             =back
283              
284              
285             =head1 LICENSE AND COPYRIGHT
286              
287             Copyright (C) 2014 Günter Grodotzki.
288              
289             This [library|program|code|module] is free software; you
290             can redistribute it and/or modify it under the same terms
291             as Perl 5.10.0. For more details, see the full text of the
292             licenses in the file LICENSE.
293              
294             This program is distributed in the hope that it will be
295             useful, but without any warranty; without even the implied
296             warranty of merchantability or fitness for a particular purpose.
297              
298             =cut
299              
300             1; # End of Passwords