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 )