Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/Yancy-*
db
django
.vscode
66 changes: 66 additions & 0 deletions lib/Digest/BcryptYancy.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package Digest::BcryptYancy;

use strictures 2;
use utf8;

use Mu;
use MooX::Clone;

use Carp 'croak';
use Crypt::Eksblowfish::Bcrypt qw( bcrypt en_base64 de_base64 );
use Data::Entropy::Algorithms 'rand_bits';

require bytes;

extends "Digest::base";

our $VERSION = '0.1';

rw _buffer => default => sub { '' };

rw cost => isa => sub {
return if defined $_[0] and $_[0] =~ /^[0-9]+$/;
croak "Cost must be a positive integer";
} => clearer => 1 => coerce => sub { sprintf "%02d", $_[0] };

rw salt => isa => sub {
return if $_[0] && $_[0] =~ /[.\/A-Za-z0-9]{22}/;
croak "Salt must be exactly 22 base 64 digits";
} => clearer => 1 => default => sub { en_base64 rand_bits 16 * 8 };

around new => sub {
my ( $orig, $class, %args ) = @_;
return $class->$orig(%args) unless #
my $settings = delete $args{settings};
$settings = de_base64 $settings;
croak "bad bcrypt settings:\n$settings"
unless $settings =~ m#\A\$2a\$([0-9]{2})\$([./A-Za-z0-9]{22})#;
$args{cost} = $1;
$args{salt} = $2;
return $class->$orig(%args);
};

sub add {
my ( $self, @data ) = @_;
$self->_buffer( join '', $self->_buffer, @data );
return $self;
}

sub b64digest { shift->digest }

sub digest {
my ($self) = @_;
my $settings = join '$', "", "2a", $self->cost, $self->salt;
my $hash = en_base64 bcrypt $self->_buffer, $settings;
$self->reset;
return $hash;
}

sub reset {
my ($self) = @_;
$self->_buffer('');
$self->$_ for map "clear_$_", qw( cost salt );
return $self;
}

1;
6 changes: 2 additions & 4 deletions lib/Yancy/Plugin/Auth/Basic.pm
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,18 @@ field is C<type>, and should be a type supported by the L<Digest> module:

=item * SHA-512 (part of core Perl)

=item * Bcrypt (recommended)
=item * BcryptYancy (recommended)

=back

Additional fields are given as configuration to the L<Digest> module.
Not all Digest types require additional configuration.

# Use Bcrypt for passwords
# Install the Digest::Bcrypt module first!
app->yancy->plugin( 'Auth::Basic' => {
password_digest => {
type => 'Bcrypt',
type => 'BcryptYancy',
cost => 12,
salt => 'abcdefgh♥stuff',
},
} );

Expand Down
33 changes: 16 additions & 17 deletions lib/Yancy/Plugin/Auth/Password.pm
Original file line number Diff line number Diff line change
Expand Up @@ -180,23 +180,21 @@ field is C<type>, and should be a type supported by the L<Digest> module:

=item * SHA-512 (part of core Perl)

=item * Bcrypt (recommended)
=item * BcryptYancy (recommended)

=back

Additional fields are given as configuration to the L<Digest> module.
Not all Digest types require additional configuration.

There is no default: Perl core provides SHA-1 hashes, but those aren't good
enough. We recommend installing L<Digest::Bcrypt> for password hashing.
enough. We recommend installing L<Digest::BcryptYancy> for password hashing.

# Use Bcrypt for passwords
# Install the Digest::Bcrypt module first!
app->yancy->plugin( 'Auth::Basic' => {
password_digest => {
type => 'Bcrypt',
type => 'BcryptYancy',
cost => 12,
salt => 'abcdefgh♥stuff',
},
} );

Expand Down Expand Up @@ -519,21 +517,22 @@ sub _get_user {
}

sub _digest_password {
my ( $self, $password ) = @_;
my ( $self, $password, $last_pw_entry ) = @_;
my $config = $self->default_digest;
my $digest_config_string = _build_digest_config_string( $config );
my $digest = _get_digest_by_config_string( $digest_config_string );
my $digest = _get_digest_by_config_string( $last_pw_entry, $digest_config_string );
my $password_string = join '$', $digest->add( $password )->b64digest, $digest_config_string;
return $password_string;
}

sub _set_password {
my ( $self, $c, $username, $password ) = @_;
my $password_string = eval { $self->_digest_password( $password ) };
my ( $self, $c, $username, $password, $last_pw_entry ) = @_;
my $password_string = eval { $self->_digest_password( $password, $last_pw_entry ) };
if ( $@ ) {
$self->log->error(
sprintf 'Error setting password for user "%s": %s', $username, $@,
);
return;
}

my $id = $self->_get_id_for_username( $c, $username );
Expand Down Expand Up @@ -700,9 +699,9 @@ sub _post_register {
}

sub _get_digest {
my ( $type, @config ) = @_;
my ( $last_pw_entry, $type, @config ) = @_;
my $digest = eval {
Digest->new( $type, @config )
Digest->new( $type, @config, settings => $last_pw_entry );
};
if ( my $error = $@ ) {
if ( $error =~ m{Can't locate Digest/${type}\.pm in \@INC} ) {
Expand All @@ -714,15 +713,15 @@ sub _get_digest {
}

sub _get_digest_by_config_string {
my ( $config_string ) = @_;
my ( $last_pw_entry, $config_string ) = @_;
my @digest_parts = split /\$/, $config_string;
return _get_digest( @digest_parts );
return _get_digest( $last_pw_entry, @digest_parts );
}

sub _build_digest_config_string {
my ( $config ) = @_;
my @config_parts = (
map { $_, $config->{$_} } grep !/^type$/, keys %$config
map { $_, $config->{$_} } grep !/^type$/, sort keys %$config
);
return join '$', $config->{type}, @config_parts;
}
Expand All @@ -739,7 +738,7 @@ sub _check_pass {
return undef;
}

my ( $user_password, $user_digest_config_string )
my ( $last_pw_entry, $user_digest_config_string )
= split /\$/, $user->{ $self->password_field }, 2;

my $force_upgrade = 0;
Expand All @@ -754,13 +753,13 @@ sub _check_pass {
$force_upgrade = 1;
}

my $digest = eval { _get_digest_by_config_string( $user_digest_config_string ) };
my $digest = eval { _get_digest_by_config_string( $last_pw_entry, $user_digest_config_string ) };
if ( $@ ) {
die sprintf 'Error checking password for user "%s": %s', $username, $@;
}
my $check_password = $digest->add( $input_password )->b64digest;

my $success = $check_password eq $user_password;
my $success = $check_password eq $last_pw_entry;

my $default_config_string = _build_digest_config_string( $self->default_digest );
if ( $success && ( $force_upgrade || $user_digest_config_string ne $default_config_string ) ) {
Expand Down
Loading