File Coverage

blib/script/bcrypt
Criterion Covered Total %
statement 76 137 55.4
branch 12 38 31.5
condition 7 21 33.3
subroutine 15 22 68.1
pod 0 5 0.0
total 110 223 49.3


line stmt bran cond sub pod time code
1             #!/usr/local/bin/perl
2              
3 28     28   160071 use v5.30;
  28         100  
4 28     28   14462 use experimental qw(signatures);
  28         137473  
  28         211  
5              
6             =encoding utf8
7              
8             =head1 NAME
9              
10             bcrypt - A command-line tool to deal with bcrypt password hashing
11              
12             =head1 SYNOPSIS
13              
14             Run this from the command line. By default, it reads one line of standard
15             input to get the password. This way the password doesn't show up in
16             your shell's history:
17              
18             $ bcrypt
19             Reading password on standard input...
20             Hello World
21             $2b$12$yQePGL/7.u6mVY8tt6fqTO08hQEc2qdWQ..FehvCwBIgMxzM0ULkq
22              
23             Control the various inputs:
24              
25             $ bcrypt --cost 10 --salt abcdef0123456789 --type 2a
26             Reading password on standard input...
27             Hello World
28             $2a$10$WUHhXETkKBCwKxOzLha2MOVweNmcwXQeoLR9jJlmEcl7rMR2ymqNu
29              
30             To check a password against a hash, use C<--compare>. Since the hashed
31             value has C<$>, which is a shell special character in various shells,
32             quote the value:
33              
34             $ perl script/bcrypt --compare '$2a$10$WUHhXETkKBCwKxOzLha2MOVweNmcwXQeoLR9jJlmEcl7rMR2ymqNu' <<<"Hello World"
35             Reading password on standard input...
36             Hello World
37             Match
38              
39             In a program, load the program as a module and call C with the
40             same strings you'd use on the command line. The return value of C
41             is the exit status:
42              
43             require '/path/to/bcrypt';
44              
45             my $exit_status = App::bcrypt::run( @command_line_strings );
46             exit $exit_status;
47              
48             =head1 DESCRIPTION
49              
50             =head2 Options
51              
52             =over 4
53              
54             =item * C<--compare> - the hash to compare to the password
55              
56             =item * C<--cost>, C<-c> - the cost factor, which should be between 5 and 31, inclusively. This is the exponent for a power of two, so things get slow quickly (default: 12)
57              
58             =item * C<--debug>, C<-d> - turn on debugging (default: off)
59              
60             =item * C<--eol>, C<-e> - the string to add to the end of the hash on output (default: newline)
61              
62             =item * C<--help>, C<-h> - show the documentation and exit. This takes precedence over all other options besides C<--update-modules> and C<--version>.
63              
64             =item * C<--no-eol>, C<-n> - do not add anything to the end of the line (default: off)
65              
66             =item * C<--password>, C<-p> - the password to hash or to compare. But, it's probably better to send it through standard input if you are using this as a command-line program and not a library.
67              
68             =item * C<--quiet>, C<-q> - suppress information messages (default: off)
69              
70             =item * C<--salt>, C<-s> - the salt to use. This is a string the should encode to 16-octet UTF-8 (default: random string)
71              
72             =item * C<--type>, C<-t> - the Bcrypt type, which is one of C<2a>, C<2b> (default), C<2x>, or C<2y>
73              
74             =item * C<--update-modules> - use cpan(1) to update or install all the Perl modules this program needs. This takes precedence over all other options.
75              
76             =item * C<--version>, C<-v> - show the version. With C<--debug>, this shows extra information about the Perl bits. This option takes precedence over all others except C<--update-modules>.
77              
78             =back
79              
80             =head2 Environment
81              
82             If these environment variables are set, they are used as the default values
83             when you do not specify a value through the command line options:
84              
85             =over 4
86              
87             =item * BCRYPT_COST
88              
89             =item * BCRYPT_EOL
90              
91             =item * BCRYPT_NO_EOL
92              
93             =item * BCRYPT_QUIET
94              
95             =item * BCRYPT_SALT
96              
97             =item * BCRYPT_TYPE
98              
99             =back
100              
101             =head2 Exit values
102              
103             =over 4
104              
105             =item * 0 - Success in either making the new hash, or that the hash and password match for C<--compare>
106              
107             =item * 1 - The hash and password for C<--compare> do not match
108              
109             =item * 2 - Invalid input
110              
111             =back
112              
113             =head2 Examples
114              
115             Install the modules using cpan if you've dropped this file into a system:
116              
117             $ bcrypt --update-modules
118              
119             Create a password with the defaults:
120              
121             $ bcrypt
122             Reading password on standard input...
123             Hello World
124              
125             Same thing, but without the extra output:
126              
127             $ perl script/bcrypt -q
128             Hello World
129             $2b$12$IqR8.HcodAzvngql3qDw2upnUrjj9HSxutWCgPrUsga0gm7AvTOUu
130              
131             By default, there's a newline at the end of the hash because it's a normal
132             output line, but you can remove that with C<--no-eol>. First, normally:
133              
134             $ bcrypt -q | hexdump -C
135             Hello World
136             00000000 24 32 62 24 31 32 24 35 4b 63 68 65 4f 6d 65 48 |$2b$12$5KcheOmeH|
137             00000010 61 67 52 62 77 2f 4a 57 70 79 54 55 65 73 70 39 |agRbw/JWpyTUesp9|
138             00000020 67 76 41 58 78 32 66 6e 79 6e 41 33 2f 6b 75 35 |gvAXx2fnynA3/ku5|
139             00000030 6a 43 50 4a 7a 70 5a 38 39 47 58 65 0a |jCPJzpZ89GXe.|
140             0000003d
141              
142             Now, notice the lack of the C<0a> at the end:
143              
144             $ bcrypt -q --no-eol | hexdump -C
145             Hello World
146             00000000 24 32 62 24 31 32 24 4d 36 67 2e 54 64 7a 48 49 |$2b$12$M6g.TdzHI|
147             00000010 44 58 52 30 73 33 66 2f 66 57 74 74 4f 61 54 70 |DXR0s3f/fWttOaTp|
148             00000020 34 2f 7a 57 5a 47 63 44 76 56 63 45 6d 6a 49 4e |4/zWZGcDvVcEmjIN|
149             00000030 50 79 4c 79 41 74 48 62 71 56 79 75 |PyLyAtHbqVyu|
150             0000003c
151              
152             Choose your own settings. The value for C<--salt> should be a string that
153             will UTF-8 encode to exactly 16 octets. Care with C<--cost> because that's
154             a power of 2:
155              
156             $ bcrypt --type 2a --cost 30 --salt abcdef0123456789
157             Reading password on standard input...
158             Hello World
159             $2a$10$WUHhXETkKBCwKxOzLha2MOVweNmcwXQeoLR9jJlmEcl7rMR2ymqNu
160              
161             Compare a hash to a password supplied on standard input:
162              
163             $ bcrypt --compare '$2a$10$WUHhXETkKBCwKxOzLha2MOVweNmcwXQeoLR9jJlmEcl7rMR2ymqNu'
164             Reading password on standard input...
165             Hello Worldd
166             Does not match
167              
168             Do the same thing quietly, but use the exit value to figure out what
169             happened. This bad password has an extra C:
170              
171             $ bcrypt --quiet --compare '$2a$10$WUHhXETkKBCwKxOzLha2MOVweNmcwXQeoLR9jJlmEcl7rMR2ymqNu'
172             Hello Worldd
173             $ echo $?
174             1
175              
176             This password is the right one:
177              
178             $ bcrypt --quiet --compare '$2a$10$WUHhXETkKBCwKxOzLha2MOVweNmcwXQeoLR9jJlmEcl7rMR2ymqNu'
179             Hello World
180             $ echo $?
181             0
182              
183             =head1 SEE ALSO
184              
185             =over 4
186              
187             =item * Crypt::Bcrypt (which this program uses)
188              
189             =item * Mojolicious::Plugin::Bcrypt
190              
191             =item * Mojolicious::Command::bcrypt
192              
193             =item * https://github.com/shoenig/bcrypt-tool, a Golang tool
194              
195             =back
196              
197             =head1 SOURCE AVAILABILITY
198              
199             This source is in Github:
200              
201             http://github.com/briandfoy/app-bcrypt
202              
203             =head1 AUTHOR
204              
205             brian d foy, C<< >>
206              
207             =head1 COPYRIGHT AND LICENSE
208              
209             Copyright © 2023, brian d foy, All Rights Reserved.
210              
211             You may redistribute this under the terms of the Artistic License 2.0.
212              
213             =cut
214              
215             package App::bcrypt;
216              
217 20     20 0 197 sub VERSION { '1.001' }
218              
219 28 50       4782866 exit run( @ARGV ) unless caller;
220              
221 26     26 0 178 sub EX_SUCCESS () { 0 }
  26         112  
  26         0  
222 0     0 0 0 sub EX_NO_MATCH () { 1 }
  0         0  
  0         0  
223 2     2 0 5 sub EX_USAGE () { 2 }
  2         4  
  2         0  
224              
225 28     28 0 108 sub run ( @args ) {
  28         111  
  28         50  
226 28 50       86 return _update_modules() if grep { $_ eq '--update-modules' } @args;
  57         178  
227              
228 28         144 my $processed_args = _process_args(\@args);
229 28 100       121 exit EX_USAGE unless defined $processed_args;
230              
231 26 100       151 return _show_version($processed_args->{'debug'}) if $processed_args->{'version'};
232 6 50       35 return _show_help() if $processed_args->{'help'};
233              
234 0 0       0 if( $processed_args->{quiet} ) {
235 28     28   22396 no warnings qw(redefine);
  28         53  
  28         114526  
236 0     0   0 *_message = sub { 1 }
237 0         0 }
238              
239 0         0 my $errors = _validate_args( $processed_args );
240              
241 0 0       0 if( $errors->@* > 0 ) {
242 0         0 foreach ( $errors->@* ) {
243 0         0 _message( $_ );
244             }
245 0         0 return EX_USAGE;
246             }
247              
248 0 0       0 if( ! defined $processed_args->{'password'} ) {
249 0         0 $processed_args->{'password'} = _read_password();
250             }
251              
252 0 0       0 if( defined $processed_args->{compare} ) {
253 0         0 state $rc = require Crypt::Bcrypt;
254 0         0 my $r = Crypt::Bcrypt::bcrypt_check( $processed_args->@{qw(password compare)} );
255              
256 0         0 return do {
257 0 0       0 if( $r ) {
258 0         0 _message("Match");
259 0         0 EX_SUCCESS;
260             }
261             else {
262 0         0 _message("Does not match");
263 0         0 EX_NO_MATCH;
264             }
265             };
266             }
267              
268 0 0       0 if( $processed_args->{'debug'} ) {
269 0         0 say _dumper($processed_args);
270             }
271              
272 0         0 state $rc = require Crypt::Bcrypt;
273 0         0 my $hashed = Crypt::Bcrypt::bcrypt( $processed_args->@{qw(password type cost salt)} );
274              
275 0         0 local $\ = do {
276 0 0       0 if( $processed_args->{'no-eol'} ) { undef }
  0 0       0  
277 0         0 elsif( $processed_args->{'eol'} ) { $processed_args->{'eol'} }
278 0         0 else { "\n" }
279             };
280              
281 0         0 print $hashed;
282              
283 0         0 return EX_SUCCESS;
284             }
285              
286 28     28   166 sub _defaults () {
  28         92  
287             my %hash = (
288             'no-eol' => $ENV{BCRYPT_NO_EOL} // 0,
289             cost => $ENV{BCRYPT_COST} // 12,
290             debug => $ENV{BCRYPT_DEBUG} // 0,
291             eol => $ENV{BCRYPT_EOL} // "\n",
292             quiet => $ENV{BCRYPT_QUIET} // 0,
293             salt => _default_salt(),
294 28   50     819 type => lc( $ENV{BCRYPT_TYPE} // '2b' ),
      50        
      50        
      50        
      50        
      50        
295             );
296              
297 28         1479 return \%hash;
298             }
299              
300 28     28   64 sub _default_salt () {
  28         57  
301 28         72 my $unencoded_salt = do {
302 28 50       116 if( exists $ENV{BCRYPT_SALT} ) {
303 0         0 state $rc = require Encode;
304             Encode::encode( 'UTF-8', $ENV{BCRYPT_SALT} )
305 0         0 }
306             else {
307 28         18238 state $rc = require Crypt::URandom;
308 28         149296 Crypt::URandom::urandom(16)
309             }
310             };
311             }
312              
313             sub _dumper {
314 0     0   0 state $rc = require Data::Dumper;
315 0         0 Data::Dumper->new([@_])->Indent(1)->Sortkeys(1)->Terse(1)->Useqq(1)->Dump
316             }
317              
318 60     60   138 sub _message ($m) {
  60         113  
  60         72  
319 60         113 chomp $m;
320 60         71 say { _message_fh() } $m;
  60         119  
321             }
322              
323 60     60   75 sub _message_fh () {
  60         62  
324 60   33     553 $App::bcrypt::fh // *STDOUT
325             }
326              
327 20     20   27 sub _modules () {
  20         22  
328 20         70 qw(Crypt::Bcrypt Crypt::URandom Encode);
329             }
330              
331 28     28   81 sub _process_args ($args) {
  28         54  
  28         60  
332 28         24076 state $c = require Getopt::Long;
333              
334 28         407486 my %opts = _defaults()->%*;
335             my %opts_description = (
336             'compare=s' => \ $opts{'compare'},
337             'cost|c=i' => \ $opts{'cost'},
338             'debug|d' => \ $opts{'debug'},
339             'eol|e=s' => \ $opts{'eol'},
340             'help|h' => \ $opts{'help'},
341             'no-eol|n' => \ $opts{'no-eol'},
342             'password|p=s' => \ $opts{'password'},
343             'quiet|q' => \ $opts{'quiet'},
344             'salt|s=s' => \ $opts{'salt'},
345             'type|t=s' => \ $opts{'type'},
346             'update-modules' => \ $opts{'update-modules'},
347 28         502 'version|v' => \ $opts{'version'},
348             );
349              
350 28         179 my $ret = Getopt::Long::GetOptionsFromArray( $args, %opts_description );
351 28 100       48480 return unless $ret;
352              
353 26         144 return \%opts;
354             }
355              
356 0     0   0 sub _read_password () {
  0         0  
357 0         0 _message( 'Reading password on standard input...' );
358 0         0 my $password = ;
359 0         0 chomp $password;
360 0         0 $password;
361             }
362              
363 6     6   10 sub _show_help () {
  6         11  
364 6         3602 state $rc = require Pod::Usage;
365 6         388295 print Pod::Usage::pod2usage(
366             -verbose => 2,
367             -exitval => 'NOEXIT',
368             );
369 6         4587527 return EX_SUCCESS;
370             }
371              
372 20     20   35 sub _show_version ($debug = 0) {
  20         41  
  20         30  
373 20         199 _message( "$0 " . __PACKAGE__->VERSION );
374 20 100       86 return EX_SUCCESS unless $debug;
375              
376 10         104 _message( "\tperl $^V at $^X" );
377              
378 10         37 my $width = ( sort { $a <=> $b } map { length } _modules() )[-1];
  20         53  
  30         77  
379              
380 10         24 foreach my $module ( sort { $a cmp $b} _modules() ) {
  30         54  
381 30         2958 my $rc = eval "require $module";
382 30         69985 my $rel_path = $module =~ s|::|/|gr . '.pm';
383 30         845 _message( sprintf "\t%-*s\n\t\t%f\n\t\t%s", $width, $module, $module->VERSION, $INC{$rel_path} )
384             }
385              
386 10         39 return EX_SUCCESS;
387             }
388              
389 0     0     sub _types () { qw( 2a 2b 2x 2y ) }
  0            
  0            
390              
391 0     0     sub _update_modules () {
  0            
392 0           state $rc = require App::Cpan;
393 0           App::Cpan->run( _modules() )
394             }
395              
396 0     0     sub _validate_args ( $processed_args ) {
  0            
  0            
397 0           my( $c, $t, $s, $p ) = $processed_args->@{qw(cost type salt password)};
398              
399 0           my @errors;
400              
401 0 0 0       push @errors, qq(The cost must be a whole number between 5 and 31, inclusively, but got "$c")
      0        
402             unless( int($c) == $c and ( 4 < $c && $c < 32 ) );
403 0           push @errors, qq(The type must be one of @{[ _types ]}, but got "$t")
404 0 0         unless grep { $t eq $_ } _types();
  0            
405 0 0         push @errors, sprintf "The salt must be 16 octets, but got %s", _dumper($s) =~ s/\R+\z//r
406             unless 16 == length $s;
407              
408 0           return \@errors;
409             }
410              
411             1;