package App::bmkpasswd;
$App::bmkpasswd::VERSION = '2.007001';
use strictures 2;
use Carp;
use Try::Tiny;

use Crypt::Eksblowfish::Bcrypt qw/
  bcrypt 
  en_base64
/;

use parent 'Exporter::Tiny';
our @EXPORT = qw/
  mkpasswd
  passwdcmp 
/;
our @EXPORT_OK = qw/
  mkpasswd_available
  mkpasswd_forked
/;

use Bytes::Random::Secure;
my ($brs, $brsnb);
sub get_brs {
  my (%params) = @_;
  $params{strong} ?
    $brs ||= Bytes::Random::Secure->new(Bits => 128)
    : $brsnb ||= Bytes::Random::Secure->new(Bits => 128, NonBlocking => 1)
}

sub mkpasswd_forked {
  srand; # wrt random-length salts
  undef $brs;
  undef $brsnb;
}

my %_can_haz;
sub have_passwd_xs {
  unless (defined $_can_haz{passwdxs}) {
    try { require Crypt::Passwd::XS;  $_can_haz{passwdxs} = 1 } 
      catch { $_can_haz{passwdxs} = 0 };
  }
  $_can_haz{passwdxs}
}

my %_shatests = (
  sha256 => sub {
    my $testc = try { crypt('a', '$5$abc$') } catch { warn $_; () };
    $testc && index($testc, '$5$abc$') == 0 ? 1 : ()
  },
  sha512 => sub {
    my $testc = try { crypt('b', '$6$abc$') } catch { warn $_; () };
    $testc && index($testc, '$6$abc$') == 0 ? 1 : ()
  },
);

sub have_sha {
  # if we have Crypt::Passwd::XS, just use that:
  return 1 if have_passwd_xs();

  # else determine (the slow way) if SHA256/512 are available via libc:
  my $rate = $_[0] || 512;
  my $type = "sha$rate";
  return $_can_haz{$type} if defined $_can_haz{$type};
  if (exists $_shatests{$type} && $_shatests{$type}->()) {
    return $_can_haz{$type} = 1
  }
  return $_can_haz{$type} = 0
}

sub have_md5 {
  return 1 if have_passwd_xs();
  return $_can_haz{md5} if defined $_can_haz{md5};
  my $testc = try { crypt('a', '$1$abcd$') } catch { warn $_; () };
  if ($testc && index($testc, '$1$abcd$') == 0) {
    return $_can_haz{md5} = 1
  }
  return $_can_haz{md5} = 0
}


sub mkpasswd_available {
  my ($type) = @_;

  unless ($type) {
    return (
      'bcrypt',
      ( have_sha(256) ? 'sha256' : () ),
      ( have_sha(512) ? 'sha512' : () ),
      ( have_md5()    ? 'md5'    : () ),
    );
  }

  $type = lc $type;
  return 1            if $type eq 'bcrypt';
  return have_sha($1) if $type =~ /^sha-?(\d{3})$/;
  return have_md5()   if $type eq 'md5';
  return
}

my $_saltgen = sub {
  my ($type, $strong) = @_;

  my $rnd = get_brs(strong => $strong);

  if ($type eq 'bcrypt') {
    return en_base64( $rnd->bytes(16) );
  }

  if ($type eq 'sha') {
    my $max = en_base64( $rnd->bytes(16) );
    my $initial = substr $max, 0, 8, '';
    # Drepper recommends random-length salts:
    $initial .= substr $max, 0, 1, '' for  1 .. rand 8;
    return $initial
  }

  if ($type eq 'md5') {
    return en_base64( $rnd->bytes(6) );
  }

  confess "_saltgen fell through, unknown type $type"
};

sub mkpasswd {
  # mkpasswd $passwd => $type, $cost, $strongsalt;
  # mkpasswd $passwd => +{
  #   type => $type,
  #   cost => $cost,
  #   saltgen => $coderef,
  #   strong => $strongsalt,
  # }
  my $pwd = shift;
  croak "mkpasswd passed an undef password"
    unless defined $pwd;

  my %opts =
    ref $_[0] eq 'HASH' ? %{ $_[0] }
    : map {; $_ => shift } qw/type cost strong/;
  my $type = defined $opts{type} ? $opts{type} : 'bcrypt';
  my $saltgen = $opts{saltgen} || $_saltgen;

  my $salt;

  TYPE: {
    if ($type =~ /^bcrypt$/i) {
      my $cost = $opts{cost} || '08';

      croak 'Work cost factor must be numeric'
        unless $cost =~ /^[0-9]+$/;
      $cost = "0$cost" if length $cost == 1;

      $salt = $saltgen->(bcrypt => $opts{strong});
      my $bsettings = join '', '$2a$', $cost, '$', $salt;

      return bcrypt($pwd, $bsettings)
    }

    if ($type =~ /^sha-?512$/i) {
      croak 'SHA hash requested but no SHA support available' 
        unless have_sha(512);
      $salt = join '', '$6$', $saltgen->(sha => $opts{strong}), '$';
      last TYPE
    }

    if ($type =~ /^sha(-?256)?$/i) {
      croak 'SHA hash requested but no SHA support available' 
        unless have_sha(256);
      $salt = join '', '$5$', $saltgen->(sha => $opts{strong}), '$';
      last TYPE
    }

    if ($type =~ /^md5$/i) {
      croak 'MD5 hash requested but no MD5 support available'
        unless have_md5;
      $salt = join '', '$1$', $saltgen->(md5 => $opts{strong}), '$';
      last TYPE
    }

    croak "Unknown type specified: $type"
  }

  have_passwd_xs() ?
    Crypt::Passwd::XS::crypt($pwd, $salt) : crypt($pwd, $salt)
}

sub passwdcmp {
  my ($pwd, $crypt) = @_;
  croak 'Expected a password string and hash'
    unless defined $pwd and $crypt;

  my $pos_a = index $crypt, '$';
  my $pos_b = index $crypt, '$', 2;
  carp 'Possibly passed an invalid hash' 
    unless $pos_a == 0
    and    $pos_b == 2
    or     $pos_b == 3;

  if ($crypt =~ /^\$2a\$\d{2}\$/) {
    # Looks like bcrypt.
    return $crypt if $crypt eq bcrypt($pwd, $crypt)
  } else {
    if (have_passwd_xs) {
      return $crypt if $crypt eq Crypt::Passwd::XS::crypt($pwd, $crypt)
    } else {
      return $crypt if $crypt eq crypt($pwd, $crypt)
    }
  }

  undef
}

1;
__END__

=pod

=head1 NAME

App::bmkpasswd - bcrypt-capable mkpasswd(1) and exported helpers

=head1 SYNOPSIS

  ## From Perl:

  use App::bmkpasswd -all;
  my $bcrypted = mkpasswd($passwd);
  say 'matched' if passwdcmp($passwd, $bcrypted);

  ## From a shell:

  bmkpasswd --help
  
  # Generate bcrypted passwords:
  bmkpasswd

  # Defaults to work cost factor '08':
  bmkpasswd --workcost='06'

  # SHA requires Crypt::Passwd::XS or a recent libc:
  bmkpasswd --method='sha512'
  
  # Compare a hash:
  bmkpasswd --check=HASH

  # Check hash generation times:
  bmkpasswd --benchmark

=head1 DESCRIPTION

B<App::bmkpasswd> is a bcrypt-enabled C<mkpasswd> implementation.

Helper functions are also exported for use in other applications; see
L</EXPORTED>.
L<Crypt::Bcrypt::Easy> provides an easier bcrypt-specific
programmatic interface for Perl programmers.

See C<bmkpasswd --help> for command-line usage information.

Uses L<Crypt::Eksblowfish::Bcrypt> for bcrypted passwords.

Bcrypt leverages a work-cost factor allowing hash generation
to become configurably slower as computers get faster, thereby
impeding brute-force hash generation attempts.
See L<http://codahale.com/how-to-safely-store-a-password/> for more
on why you ought to be using bcrypt or similar "adaptive" techniques.

B<SHA-256> and B<SHA-512> are supported if available. SHA support requires
either L<Crypt::Passwd::XS> or a system crypt() that can handle SHA (such as
glibc-2.7+ or modern FreeBSD builds).

Uses L<Bytes::Random::Secure> to generate random salts. Strongly-random salts
can also be enabled; see L</mkpasswd>.

=head1 EXPORTED

L<Crypt::Bcrypt::Easy> provides an easier programmatic interface, if you're
only interested in generating bcrypt passwords.  If you'd like to make use of
other password types, you can use the exported B<mkpasswd> and B<passwdcmp>
functions:

  # Import selectively:
  use App::bmkpasswd 'mkpasswd', 'passwdcmp';
  # Or import all functions:
  use App::bmkpasswd -all;

This module uses L<Exporter::Tiny> to export functions. This provides for
flexible import options. See the L<Exporter::Tiny> docs for details.

=head2 passwdcmp

Compare a password against a hash.

  if ( passwdcmp($plaintext, $crypted) ) {
    ## Successful match
  } else {
    ## Failed match
  }

B<passwdcmp> will return the hash if it is a match; otherwise, C<undef>
is returned. (This is an API change in C<v2.7.1>; prior versions return
an empty list on failure.)

=head2 mkpasswd

  my $crypted = mkpasswd($passwd);
  my $crypted = mkpasswd($passwd, $type);
  my $crypted = mkpasswd($passwd, 'bcrypt', $cost);
  my $crypted = mkpasswd($passwd, $type, $cost, $strongsalt);

  my $crypted = mkpasswd( $passwd => 
    +{
      type    => $type,
      cost    => $cost,
      strong  => $strongsalt,
      saltgen => $saltgenerator,
    }
  );

Generate hashed passwords.

By default, generates a bcrypted passwd with work-cost 08:

  $bcrypted = mkpasswd($passwd);

A different work-cost can be specified for bcrypt passwds:

  $bcrypted = mkpasswd($passwd, 'bcrypt', '10');

SHA-256 and SHA-512 are supported, in which case the work-cost value is ignored:

  $crypted = mkpasswd($passwd, 'sha256');
  $crypted = mkpasswd($passwd, 'sha512');

If a fourth boolean-true argument is specified, a strongly-random salt is
generated. This requires spare entropy, and will block if entropy-starved:

  $crypted = mkpasswd($passwd, 'bcrypt', '08', 'strong');
  $crypted = mkpasswd($passwd, 'sha512', 0, 'strong');

Options can be passed as a HASH, instead. This also lets you pass in a salt
generator coderef:

  $crypted = mkpasswd( $passwd => +{
      type => 'bcrypt',
      cost => '10',
      strong  => 0,
      saltgen => $saltgenerator,
    }
  );

The salt generator is passed the type (one of: C<bcrypt>, C<sha>, C<md5>) and
the value of the B<strong> option (default false).

  my $saltgenerator = sub {
    my ($type, $strongsalt) = @_;
    if ($type eq 'bcrypt') {
      # ...
    } elsif ($type eq 'sha') {
      # ...
    } else {
      die "Don't know how to create a salt for type '$type'!"
    }
  };

Most people want random salts, in which case the default salt generator
should be fine.

See L</mkpasswd_forked> if your application loads this module before forking
or creating threads that generate passwords.

=head2 mkpasswd_available

  my @available = mkpasswd_available;

  if ( mkpasswd_available('sha512') ) { ... }

Given no arguments, returns the list of available hash types.

Given a type (see L</mkpasswd>), returns boolean true if the method is available. ('bcrypt' is
always available.)

=head2 mkpasswd_forked

  # After a fork / new thread is created:
  mkpasswd_forked;

To retain secure salts after forking the process or creating a new thread, 
it's advisable to either only load this module after creating the new process
or call B<mkpasswd_forked> in the new process to reset the random seeds used
by salt generators.

Added in C<v2.6.1>.

=head1 AUTHOR

Jon Portnoy <avenj@cobaltirc.org>

=for Pod::Coverage have_\w+ get_\w+

=cut
