[freeside-commits] branch master updated. dbb1f2c9894385044ed85b64d9016b2eeb06d649

Jonathan Prykop jonathan at 420.am
Sat Dec 17 14:42:35 PST 2016


The branch, master has been updated
       via  dbb1f2c9894385044ed85b64d9016b2eeb06d649 (commit)
      from  61e54f288c3b6c93bcfdf128c8117f66965f463b (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit dbb1f2c9894385044ed85b64d9016b2eeb06d649
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Sat Dec 17 16:41:45 2016 -0600

    73085: Enable credit card/ach encryption on a live system

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;
+

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Setup.pm                      |    7 ++-
 FS/FS/Upgrade.pm                    |    1 +
 FS/FS/cust_main.pm                  |   81 ++++++++++++++++++++++++++
 FS/t/suite/15-activate_encryption.t |  106 +++++++++++++++++++++++++++++++++++
 4 files changed, 194 insertions(+), 1 deletion(-)
 create mode 100755 FS/t/suite/15-activate_encryption.t




More information about the freeside-commits mailing list