diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index c936082665..6d798b2f03 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -831,6 +831,13 @@ my $validate_email = sub { $_[0] =~ 'type' => 'checkbox', }, + { + 'key' => 'hashsalt', + 'section' => 'billing', + 'description' => 'Hash salt string (enables hashing of session keys/cookies). Changing salt string will force a new login for all sessions in progress.', + 'type' => 'text', + }, + { 'key' => 'encryption', 'section' => 'billing', diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index 70d4f672ec..33d7217a5d 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -22,6 +22,7 @@ use FS::Schema qw(dbdef); use FS::SearchCache; use FS::Msgcat qw(gettext); #use FS::Conf; #dependency loop bs, in install_callback below instead +use Digest::SHA qw(sha512_hex); use FS::part_virtual_field; @@ -59,6 +60,7 @@ our $conf_encryption = ''; our $conf_encryptionmodule = ''; our $conf_encryptionpublickey = ''; our $conf_encryptionprivatekey = ''; +our $conf_hashsalt = ''; FS::UID->install_callback( sub { eval "use FS::Conf;"; @@ -72,6 +74,7 @@ FS::UID->install_callback( sub { my $nw_coords = $conf->exists('geocode-require_nw_coordinates'); $lat_lower = $nw_coords ? 1 : -90; $lon_upper = $nw_coords ? -1 : 180; + $conf_hashsalt = $conf->config('hashsalt'); $File::CounterFile::DEFAULT_DIR = $conf->base_dir . "/counters.". datasrc; @@ -430,6 +433,18 @@ sub qsearch { ) { my $value = $record->{$field}; + # If searching for the user session, search for SHA512'ed + # (Also works for other ::hashed_fields ...) + if ( $conf_hashsalt + && defined(eval '@FS::'. $stable . '::hashed_fields') + && scalar( eval '@FS::'. $stable . '::hashed_fields') + ) { + foreach my $hashed_field_name (eval '@FS::'. $stable . '::hashed_fields') { + next if $hashed_field_name ne $field; # continue if this isn't a hashed field + $value = sha512_hex($value.$conf_hashsalt); + } + } + my $op = (ref($value) && $value->{op}) ? $value->{op} : '='; $value = $value->{'value'} if ref($value); my $type = dbdef->table($table)->column($field)->type; @@ -1299,9 +1314,9 @@ sub insert { my $table = $self->table; # Encrypt before the database - if ( defined(eval '@FS::'. $table . '::encrypted_fields') + if ( $conf_encryption + && defined(eval '@FS::'. $table . '::encrypted_fields') && scalar( eval '@FS::'. $table . '::encrypted_fields') - && $conf_encryption ) { foreach my $field (eval '@FS::'. $table . '::encrypted_fields') { next if $field eq 'payinfo' @@ -1314,6 +1329,17 @@ sub insert { } } + # SHA512 before the database + if ( $conf_hashsalt + && defined(eval '@FS::'. $table . '::hashed_fields') + && scalar( eval '@FS::'. $table . '::hashed_fields') + ) { + foreach my $field (eval '@FS::'. $table . '::hashed_fields') { + $saved->{$field} = $self->getfield($field); + $self->setfield($field, sha512_hex($self->getfield($field).$conf_hashsalt)); + } + } + #false laziness w/delete my @real_fields = grep { defined($self->getfield($_)) && $self->getfield($_) ne "" } diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index c8b9b631da..d307810099 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -5623,7 +5623,7 @@ sub tables_hashref { 'access_user_session' => { 'columns' => [ 'sessionnum', 'serial', '', '', '', '', - 'sessionkey', 'varchar', '', $char_d, '', '', + 'sessionkey', 'varchar', '', 128, '', '', 'usernum', 'int', '', '', '', '', 'start_date', @date_type, '', '', 'last_date', @date_type, '', '', diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index ffc04bab77..c14cc7cb95 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -156,6 +156,22 @@ If you need to continue using the old Form 477 report, turn on the enable_banned_pay_pad() unless length($conf->config('banned_pay-pad')); + # generate initial hashsalt (for sessionkeys) if it doesn't exist already + my @conf_hashsalt_key = grep { /^hashsalt$/ } map { $_->{'key'} } @FS::Conf::config_items; + if (scalar(@conf_hashsalt_key)) { # using hash salts + my $hashsalt_entry = qsearchs('conf', { 'name' => 'hashsalt'}); + unless ($hashsalt_entry) { + my $salt_length = int(rand(26)) + 25; # between 25 and 50 inclusive + my @chars = ("A" .. "Z", "a" .. "z", 0 .. 9, qw(! $ % ^ *) ); + my $init_salt = join("", @chars[ map { rand @chars } ( 1 .. $salt_length ) ]); + my $hs = new FS::conf { + 'name' => 'hashsalt', + 'value' => $init_salt, + }; + my $ret = $hs->insert; + warn "Unable to insert default hash salt for session keys\n" if $ret; + } + } } sub upgrade_overlimit_groups { diff --git a/FS/FS/access_user_session.pm b/FS/FS/access_user_session.pm index 7845d92aa5..d56cd1b808 100644 --- a/FS/FS/access_user_session.pm +++ b/FS/FS/access_user_session.pm @@ -3,6 +3,8 @@ use base qw( FS::Record ); use strict; +our @hashed_fields = ('sessionkey'); + =head1 NAME FS::access_user_session - Object methods for access_user_session records