summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Prykop <jonathan@freeside.biz>2016-12-17 16:41:45 -0600
committerJonathan Prykop <jonathan@freeside.biz>2016-12-17 16:41:45 -0600
commitdbb1f2c9894385044ed85b64d9016b2eeb06d649 (patch)
tree1e090590df43f30ae8ccda37481cf1cb8e297b0c
parent61e54f288c3b6c93bcfdf128c8117f66965f463b (diff)
73085: Enable credit card/ach encryption on a live system
-rw-r--r--FS/FS/Setup.pm7
-rw-r--r--FS/FS/Upgrade.pm1
-rw-r--r--FS/FS/cust_main.pm81
-rwxr-xr-xFS/t/suite/15-activate_encryption.t106
4 files changed, 194 insertions, 1 deletions
diff --git a/FS/FS/Setup.pm b/FS/FS/Setup.pm
index 0c3226a..f005a36 100644
--- a/FS/FS/Setup.pm
+++ b/FS/FS/Setup.pm
@@ -7,7 +7,6 @@ use vars qw( @EXPORT_OK );
use Tie::IxHash;
use Crypt::OpenSSL::RSA;
use FS::UID qw( dbh driver_name );
-#use FS::Record;
use FS::svc_domain;
$FS::svc_domain::whois_hack = 1;
@@ -99,6 +98,12 @@ sub enable_encryption {
$conf->set('encryptionpublickey', $rsa->get_public_key_string );
$conf->set('encryptionprivatekey', $rsa->get_private_key_string );
+ # reload Record globals, false laziness with FS::Record
+ $FS::Record::conf_encryption = $conf->exists('encryption');
+ $FS::Record::conf_encryptionmodule = $conf->config('encryptionmodule');
+ $FS::Record::conf_encryptionpublickey = join("\n",$conf->config('encryptionpublickey'));
+ $FS::Record::conf_encryptionprivatekey = join("\n",$conf->config('encryptionprivatekey'));
+
}
sub enable_banned_pay_pad {
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index 41349a5..27c4b4c 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -367,6 +367,7 @@ sub upgrade_data {
'agent_payment_gateway' => [],
#cust_main (tokenizes cards, remove paycvv from history, locations, cust_payby, etc)
+ # (handles payinfo encryption/tokenization across all relevant tables)
'cust_main' => [],
#contact -> cust_contact / prospect_contact
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 51bde33..493b1c6 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -5354,13 +5354,94 @@ sub _upgrade_data { #class method
$class->_upgrade_otaker(%opts);
+ # turn on encryption as part of regular upgrade, so all new records are immediately encrypted
+ # existing records will be encrypted in queueable_upgrade (below)
+ unless ($conf->exists('encryptionpublickey') || $conf->exists('encryptionprivatekey')) {
+ eval "use FS::Setup";
+ die $@ if $@;
+ FS::Setup::enable_encryption();
+ }
+
}
sub queueable_upgrade {
my $class = shift;
+
+ ### encryption gets turned on in _upgrade_data, above
+
+ eval "use FS::upgrade_journal";
+ die $@ if $@;
+
+ # prior to 2013 (commit f16665c9) payinfo was stored in history if not encrypted,
+ # clear that out before encrypting/tokenizing anything else
+ if (!FS::upgrade_journal->is_done('clear_payinfo_history')) {
+ foreach my $table ('cust_payby','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') {
+ my $sql = 'UPDATE h_'.$table.' SET payinfo = NULL WHERE payinfo IS NOT NULL';
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+ }
+ FS::upgrade_journal->set_done('clear_payinfo_history');
+ }
+
+ # encrypt old records
+ if ($conf->exists('encryption') && !FS::upgrade_journal->is_done('encryption_check')) {
+
+ # allow replacement of closed cust_pay/cust_refund records
+ local $FS::payinfo_Mixin::allow_closed_replace = 1;
+
+ # because it looks like nothing's changing
+ local $FS::Record::no_update_diff = 1;
+
+ # commit everything immediately
+ local $FS::UID::AutoCommit = 1;
+
+ # encrypt what's there
+ foreach my $table ('cust_payby','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') {
+ my $tclass = 'FS::'.$table;
+ my $lastrecnum = 0;
+ my @recnums = ();
+ while (my $recnum = _upgrade_next_recnum(dbh,$table,\$lastrecnum,\@recnums)) {
+ my $record = $tclass->by_key($recnum);
+ next unless $record; # small chance it's been deleted, that's ok
+ next unless grep { $record->payby eq $_ } @FS::Record::encrypt_payby;
+ # window for possible conflict is practically nonexistant,
+ # but just in case...
+ $record = $record->select_for_update;
+ my $error = $record->replace;
+ die $error if $error;
+ }
+ }
+
+ FS::upgrade_journal->set_done('encryption_check');
+ }
+
+ # now that everything's encrypted, tokenize...
FS::cust_main::Billing_Realtime::token_check(@_);
}
+# not entirely false laziness w/ Billing_Realtime::_token_check_next_recnum
+# cust_payby might get deleted while this runs
+# not a method!
+sub _upgrade_next_recnum {
+ my ($dbh,$table,$lastrecnum,$recnums) = @_;
+ my $recnum = shift @$recnums;
+ return $recnum if $recnum;
+ my $tclass = 'FS::'.$table;
+ my $sql = 'SELECT '.$tclass->primary_key.
+ ' FROM '.$table.
+ ' WHERE '.$tclass->primary_key.' > '.$$lastrecnum.
+ ' ORDER BY '.$tclass->primary_key.' LIMIT 500';;
+ my $sth = $dbh->prepare($sql) or die $dbh->errstr;
+ $sth->execute() or die $sth->errstr;
+ my @recnums;
+ while (my $rec = $sth->fetchrow_hashref) {
+ push @$recnums, $rec->{$tclass->primary_key};
+ }
+ $sth->finish();
+ $$lastrecnum = $$recnums[-1];
+ return shift @$recnums;
+}
+
=back
=head1 BUGS
diff --git a/FS/t/suite/15-activate_encryption.t b/FS/t/suite/15-activate_encryption.t
new file mode 100755
index 0000000..e5732f7
--- /dev/null
+++ b/FS/t/suite/15-activate_encryption.t
@@ -0,0 +1,106 @@
+#!/usr/bin/perl
+
+use strict;
+use FS::Test;
+use Test::More tests => 13;
+use FS::Conf;
+use FS::UID qw( dbh );
+use DateTime;
+use FS::cust_main; # to load all other tables
+
+my $fs = FS::Test->new( user => 'admin' );
+my $conf = FS::Conf->new;
+my $err;
+my @tables = qw(cust_payby cust_pay_pending cust_pay cust_pay_void cust_refund);
+
+### can only run on test database (company name "Freeside Test")
+like( $conf->config('company_name'), qr/^Freeside Test/, 'using test database' ) or BAIL_OUT('');
+
+### we need to unencrypt our test db before we can test turning it on
+
+# temporarily load all payinfo into memory
+my %payinfo = ();
+foreach my $table (@tables) {
+ $payinfo{$table} = {};
+ foreach my $record ($fs->qsearch({ table => $table })) {
+ next unless grep { $record->payby eq $_ } @FS::Record::encrypt_payby;
+ $payinfo{$table}{$record->get($record->primary_key)} = $record->get('payinfo');
+ }
+}
+
+# turn off encryption
+foreach my $config ( qw(encryption encryptionmodule encryptionpublickey encryptionprivatekey) ) {
+ $conf->delete($config);
+ ok( !$conf->exists($config), "deleted $config" ) or BAIL_OUT('');
+}
+$FS::Record::conf_encryption = $conf->exists('encryption');
+$FS::Record::conf_encryptionmodule = $conf->config('encryptionmodule');
+$FS::Record::conf_encryptionpublickey = join("\n",$conf->config('encryptionpublickey'));
+$FS::Record::conf_encryptionprivatekey = join("\n",$conf->config('encryptionprivatekey'));
+
+# save unencrypted values
+foreach my $table (@tables) {
+ local $FS::payinfo_Mixin::allow_closed_replace = 1;
+ local $FS::Record::no_update_diff = 1;
+ local $FS::UID::AutoCommit = 1;
+ my $tclass = 'FS::'.$table;
+ foreach my $key (keys %{$payinfo{$table}}) {
+ my $record = $tclass->by_key($key);
+ $record->payinfo($payinfo{$table}{$key});
+ $err = $record->replace;
+ last if $err;
+ }
+}
+ok( !$err, "save unencrypted values" ) or BAIL_OUT($err);
+
+# make sure it worked
+CHECKDECRYPT:
+foreach my $table (@tables) {
+ my $tclass = 'FS::'.$table;
+ foreach my $key (sort {$a <=> $b} keys %{$payinfo{$table}}) {
+ my $sql = 'SELECT * FROM '.$table.
+ ' WHERE payinfo LIKE \'M%\''.
+ ' AND char_length(payinfo) > 80'.
+ ' AND '.$tclass->primary_key.' = '.$key;
+ my $sth = dbh->prepare($sql) or BAIL_OUT(dbh->errstr);
+ $sth->execute or BAIL_OUT($sth->errstr);
+ if (my $hashrec = $sth->fetchrow_hashref) {
+ $err = $table.' '.$key.' encrypted';
+ last CHECKDECRYPT;
+ }
+ }
+}
+ok( !$err, "all values unencrypted" ) or BAIL_OUT($err);
+
+### now, run upgrade
+$err = system('freeside-upgrade','admin');
+ok( !$err, 'upgrade ran' ) or BAIL_OUT('Error string: '.$!);
+
+# check that confs got set
+foreach my $config ( qw(encryption encryptionmodule encryptionpublickey encryptionprivatekey) ) {
+ ok( $conf->exists($config), "$config was set" ) or BAIL_OUT('');
+}
+
+# check that known records got encrypted
+CHECKENCRYPT:
+foreach my $table (@tables) {
+ my $tclass = 'FS::'.$table;
+ foreach my $key (sort {$a <=> $b} keys %{$payinfo{$table}}) {
+ my $sql = 'SELECT * FROM '.$table.
+ ' WHERE payinfo LIKE \'M%\''.
+ ' AND char_length(payinfo) > 80'.
+ ' AND '.$tclass->primary_key.' = '.$key;
+ my $sth = dbh->prepare($sql) or BAIL_OUT(dbh->errstr);
+ $sth->execute or BAIL_OUT($sth->errstr);
+ unless ($sth->fetchrow_hashref) {
+ $err = $table.' '.$key.' not encrypted';
+ last CHECKENCRYPT;
+ }
+ }
+}
+ok( !$err, "all values encrypted" ) or BAIL_OUT($err);
+
+exit;
+
+1;
+