App-bcrypt

 view release on metacpan or  search on metacpan

script/bcrypt  view on Meta::CPAN

=item * https://github.com/shoenig/bcrypt-tool, a Golang tool

=back

=head1 SOURCE AVAILABILITY

This source is in Github:

	http://github.com/briandfoy/app-bcrypt

=head1 AUTHOR

brian d foy, C<< <brian d foy> >>

=head1 COPYRIGHT AND LICENSE

Copyright © 2023, brian d foy, All Rights Reserved.

You may redistribute this under the terms of the Artistic License 2.0.

=cut

package App::bcrypt;

sub VERSION { '1.001' }

exit run( @ARGV ) unless caller;

sub EX_SUCCESS  () { 0 }
sub EX_NO_MATCH () { 1 }
sub EX_USAGE    () { 2 }

sub run ( @args ) {
	return _update_modules() if grep { $_ eq '--update-modules' } @args;

	my $processed_args = _process_args(\@args);
	exit EX_USAGE unless defined $processed_args;

	return _show_version($processed_args->{'debug'}) if $processed_args->{'version'};
	return _show_help() if $processed_args->{'help'};

	if( $processed_args->{quiet} ) {
		no warnings qw(redefine);
		*_message = sub { 1 }
		}

	my $errors = _validate_args( $processed_args );

	if( $errors->@* > 0 ) {
		foreach ( $errors->@* ) {
			_message( $_ );
			}
		return EX_USAGE;
		}

	if( ! defined $processed_args->{'password'} ) {
		$processed_args->{'password'} = _read_password();
		}

	if( defined $processed_args->{compare} ) {
		state $rc = require Crypt::Bcrypt;
		my $r = Crypt::Bcrypt::bcrypt_check( $processed_args->@{qw(password compare)} );

		return do {
			if( $r ) {
				_message("Match");
				EX_SUCCESS;
				}
			else {
				_message("Does not match");
				EX_NO_MATCH;
				}
			};
		}

	if( $processed_args->{'debug'} ) {
		say _dumper($processed_args);
		}

	state $rc = require Crypt::Bcrypt;
	my $hashed = Crypt::Bcrypt::bcrypt( $processed_args->@{qw(password type cost salt)} );

	local $\ = do {
		if( $processed_args->{'no-eol'} ) { undef }
		elsif( $processed_args->{'eol'} ) { $processed_args->{'eol'} }
		else                              { "\n" }
		};

	print $hashed;

	return EX_SUCCESS;
	}

sub _defaults () {
	my %hash = (
		'no-eol'  => $ENV{BCRYPT_NO_EOL}   // 0,
		cost      => $ENV{BCRYPT_COST}     // 12,
		debug     => $ENV{BCRYPT_DEBUG}    // 0,
		eol       => $ENV{BCRYPT_EOL}      // "\n",
		quiet     => $ENV{BCRYPT_QUIET}    // 0,
		salt      => _default_salt(),
		type      => lc( $ENV{BCRYPT_TYPE} // '2b' ),
		);

	return \%hash;
	}

sub _default_salt () {
	my $unencoded_salt = do {
		if( exists $ENV{BCRYPT_SALT} ) {
			state $rc = require Encode;
			Encode::encode( 'UTF-8', $ENV{BCRYPT_SALT} )
			}
		else {
			state $rc = require Crypt::URandom;
			Crypt::URandom::urandom(16)
			}
		};
	}

sub _dumper {
	state $rc = require Data::Dumper;
	Data::Dumper->new([@_])->Indent(1)->Sortkeys(1)->Terse(1)->Useqq(1)->Dump
	}

sub _message ($m) {
	chomp $m;
	say { _message_fh() } $m;
	}

sub _message_fh () {
	$App::bcrypt::fh // *STDOUT
	}

sub _modules () {
	qw(Crypt::Bcrypt Crypt::URandom Encode);
	}

sub _process_args ($args) {
	state $c = require Getopt::Long;

	my %opts = _defaults()->%*;
	my %opts_description = (
		'compare=s'      => \ $opts{'compare'},
		'cost|c=i'       => \ $opts{'cost'},
		'debug|d'        => \ $opts{'debug'},
		'eol|e=s'        => \ $opts{'eol'},
		'help|h'         => \ $opts{'help'},
		'no-eol|n'       => \ $opts{'no-eol'},
		'password|p=s'   => \ $opts{'password'},
		'quiet|q'        => \ $opts{'quiet'},
		'salt|s=s'       => \ $opts{'salt'},
		'type|t=s'       => \ $opts{'type'},
		'update-modules' => \ $opts{'update-modules'},
		'version|v'      => \ $opts{'version'},
		);

	my $ret = Getopt::Long::GetOptionsFromArray( $args, %opts_description );
	return unless $ret;

	return \%opts;
	}

sub _read_password () {
	_message( 'Reading password on standard input...' );
	my $password = <STDIN>;
	chomp $password;
	$password;
	}

sub _show_help () {
	state $rc = require Pod::Usage;
	print Pod::Usage::pod2usage(
		-verbose => 2,
		-exitval => 'NOEXIT',
		);
	return EX_SUCCESS;
	}

sub _show_version ($debug = 0) {
	_message( "$0 " . __PACKAGE__->VERSION );
	return EX_SUCCESS unless $debug;

	_message( "\tperl $^V at $^X" );

	my $width = ( sort { $a <=> $b } map { length } _modules() )[-1];

	foreach my $module ( sort { $a cmp $b} _modules() ) {
		my $rc = eval "require $module";
		my $rel_path = $module =~ s|::|/|gr . '.pm';
		_message( sprintf "\t%-*s\n\t\t%f\n\t\t%s", $width, $module, $module->VERSION, $INC{$rel_path} )
		}

	return EX_SUCCESS;
	}

sub _types () { qw( 2a 2b 2x 2y ) }

sub _update_modules () {
	state $rc = require App::Cpan;
	App::Cpan->run( _modules() )
	}

sub _validate_args ( $processed_args ) {
	my( $c, $t, $s, $p ) = $processed_args->@{qw(cost type salt password)};

	my @errors;

	push @errors, qq(The cost must be a whole number between 5 and 31, inclusively, but got "$c")
		unless( int($c) == $c and ( 4 < $c && $c < 32 ) );
	push @errors, qq(The type must be one of @{[ _types ]}, but got "$t")
		unless grep { $t eq $_ } _types();
	push @errors, sprintf "The salt must be 16 octets, but got %s", _dumper($s) =~ s/\R+\z//r
		unless 16 == length $s;

	return \@errors;
	}

1;



( run in 0.691 second using v1.01-cache-2.11-cpan-39bf76dae61 )