DBIx-Class-BcryptColumn
view release on metacpan or search on metacpan
lib/DBIx/Class/BcryptColumn.pm view on Meta::CPAN
my ($self, $column, $info) = @_;
return unless exists $info->{bcrypt};
my %info = $info->{bcrypt} eq '1' ? () : %{$info->{bcrypt}};
$info{cost} = $self->default_cost unless exists $info{cost};
return %info;
}
sub _inject_check_method {
my ($self, $check_name, $check_subref) = @_;
no strict 'refs';
*$check_name = Sub::Name::subname $check_name => $check_subref;
}
sub bcrypt {
my ($self, $password, $cost) = @_;
return Crypt::Bcrypt::bcrypt($password, '2b', $cost, $self->generate_salt);
}
sub register_column {
my ($self, $column, $info, @rest) = @_;
$self->next::method($column, $info, @rest);
return unless my %info = $self->_get_normalized_bcrypt_args($column, $info);
$self->_bcrypt_columns_info({
%{ $self->_bcrypt_columns_info || {} },
$column => \%info,
});
my $method_name = $info{'check_method'} || $self->_default_check_method_format($column, $info);
my $check_subref = $self->_default_check_generator($column, %info);
my $check_name = join q[::] => $self->result_class, $method_name;
$self->_inject_check_method($check_name, $check_subref);
}
sub bcrypt_columns {
my $self = shift;
return my @columns = keys %{ $self->_bcrypt_columns_info||+{} };
}
sub _bcrypt_set_columns {
my $self = shift;
my @columns = $self->bcrypt_columns;
foreach my $column (@columns) {
next unless $self->is_column_changed($column) || !$self->in_storage; # Don't hash unless changed (TODO: Is this premature optimization?)
my $value = $self->get_column($column);
my %info = %{$self->_bcrypt_columns_info->{$column}};
$self->set_column($column, $self->bcrypt($value, $info{cost}));
}
}
sub insert {
my $self = shift;
$self->_bcrypt_set_columns;
$self->next::method(@_);
}
sub update {
my ($self, $upd, @rest) = @_;
if (ref $upd) {
my @columns = $self->bcrypt_columns;
foreach my $column (@columns) {
next unless exists $upd->{$column};
$self->set_column($column => delete $upd->{$column})
}
}
$self->_bcrypt_set_columns;
$self->next::method($upd, @rest);
}
1;
=head1 NAME
DBIx::Class::BcryptColumn - Set a column to securely hash on insert/update using bcrypt
=head1 SYNOPSIS
__PACKAGE__->load_components('BcryptColumn');
__PACKAGE__->add_columns(
password => {
data_type => 'text',
bcrypt => 1, # Or a hashref of option overrides, see below
},
);
=head1 DESCRIPTION
It's considered best practice to store credential data about your system users (such as passwords)
using a one way hashing algorithm. That way if your system gets hack and your database becomes
compromised then at least the hackers won't know everyone's password. It also is useful as a
protective measure against in-house bad actors who have access to your production system as part
of their regular job duties.
There's a few distributions on CPAN to make it easier to do this with L<DBIx::Class>. The two most
commonly cited are L<DBIx::Class::PassphraseColumn> and L<DBIx::Class::EncodedColumn>. Those are
both good choices for this problem and all things equal you might want to review those before making
a final choice. The main reason I wrote this was to solve two issues for me. First, both of those
perform hashing on C<new> or C<set_column> instead of on insert / update as this module does. That
approach is considered more secure by the DBIC community since it means that there is never a time
where unhashed passwords are in DBIC code and if you have a core dump or similar error those plain
text passwords have no chance of ending up in a file readable by an unauthorized person. However if
you are using L<Valiant> and its DBIC glue L<DBIx::Class::Valiant> this means you can't apply any
validation rules at the DBIC level such as minimal password complexity, or do things like use the
confirmation validator, since hashing on C<new> / C<set_column> would happen before validation occurs
(In L<DBIx::Class::Valiant> validation doesn't happen until you try to update / insert the data, or if
you manually invoke C<validate>). So For L<Valiant> users I wrote this as an option to allow you
to do those things if you are willing to accept the additional risk of plain text passwords in live
memory. Personal I find this to be a minimal additional risk since it's likely those password will reside
in other parts of the code memory space anyway (such as in L<Catalyst::Request>). If this risk
bothers you and you still want to use L<DBIx::Class::Valiant> then you can do password validation work
prior to sending the data to L<DBIx::Class>. For example if you are using L<Catalyst> you can invoke
some validation work from the controller before sending parameters to DBIC.
As a second difference, this distribution only does hashing using the bcrypt algorithm (via
L<Crypt::Eksblowfish::Bcrypt>). As of late 2021 this is my goto hashing algorithm and the defaults
I have set should be sufficient to protect you against all but nation state level hackers. You can
( run in 1.125 second using v1.01-cache-2.11-cpan-39bf76dae61 )