#################### main pod documentation begin ################### =head1 NAME Zymonic::Decryptor - Zymonic Decryptor base module. =head1 SYNOPSIS TODO =head1 DESCRIPTION TODO =head1 USAGE System options decryptor_db - decryptor database name decryptor_dbusername - username for decryptor database name decryptor_dbpassword - password for decryptor database name encrypted_data_store - where to store encrypted data (self or decryptor) keydir - The directory containing the public key. =head1 BUGS None we're aware of... =head1 SUPPORT As in the license, Zymonic is provided without warranty or support unless purchased separately, however... If you email zymonic-support@zednax.com your issue will be noted and may receive a response. For security issues, please contact zymonic-security@zednax.com and someone will respond within 8 working hours. =head1 AUTHOR Alex Masidlover et al. CPAN ID: MODAUTHOR Zednax Limited alex.masidlover@zednax.com http://www.zednax.com =head1 COPYRIGHT This program is free software licensed under the... Zymonic Public License 1.0 The full text of the license can be found in the LICENSE file included with this module. Other licenses may be acceptable if including parts of Zymonic in larger projects, please contact Zednax for details. =head1 SEE ALSO perl(1). =cut #################### main pod documentation end ################### package Zymonic::Decryptor::Client; use strict; use warnings; BEGIN { use Exporter (); use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); $VERSION = '0.01'; @ISA = qw(Exporter); #Give a hoot don't pollute, do not export more than needed by default @EXPORT = qw(); @EXPORT_OK = qw(); %EXPORT_TAGS = (); } use base 'Zymonic::Decryptor'; use Zymonic::DB; use Zymonic::Utils qw(zymonic_log rethrow_exception debug_function_start debug_function_stop debug); use Zymonic::Table; use Crypt::OpenSSL::RSA; use Data::Dumper; use MIME::Base64; use Zymonic::UA; use HTTP::Request::Common qw(POST); use Storable qw(nfreeze thaw); use JSON::XS; use IO::Socket::SSL; use Zymonic; use Exception::Class ( 'Zymonic::Exception::Decryptor' => { isa => 'Zymonic::Exception', fields => ['decryptor'], description => 'Decryptor related exception' }, 'Zymonic::Exception::Decryptor::Client' => { isa => 'Zymonic::Exception::Decryptor', fields => [], description => 'Decryptor Client related exception' }, 'Zymonic::Exception::Decryptor::Client::MissingDB' => { isa => 'Zymonic::Exception::Decryptor::Client', fields => [], description => 'Decryptor Client requires DB' }, 'Zymonic::Exception::Decryptor::Client::NoDecryptor' => { isa => 'Zymonic::Exception::Decryptor::Client', fields => [], description => 'Failed to get a decryptor connection.' }, 'Zymonic::Exception::Decryptor::Client::ConnectFailed' => { isa => 'Zymonic::Exception::Decryptor::Client', fields => [ 'decryptor_url', 'status_code', 'message' ], description => 'Failed to connect to reserved decryptor.' }, 'Zymonic::Exception::Decryptor::Client::Timeout' => { isa => 'Zymonic::Exception::Decryptor::Client', fields => [ 'decryptor', 'timeout' ], description => 'Connection to decryptor timed out after [[timeout]]s.' }, 'Zymonic::Exception::Decryptor::Client::InvalidResponse' => { isa => 'Zymonic::Exception::Decryptor::Client', fields => [ 'response', 'decryptor' ], description => 'Invalid Decryptor Response - [[response]]' }, 'Zymonic::Exception::Decryptor::Client::ErrorResponse' => { isa => 'Zymonic::Exception::Decryptor::Client', fields => [ 'response', 'decryptor' ], description => 'Invalid Decryptor Response - [[response]]' }, 'Zymonic::Exception::Decryptor::Client::SIMData' => { isa => 'Zymonic::Exception::Decryptor::Client', fields => [], description => 'store_in_mem data must not contain ###' }, ); use Net::hostent; our $timeout = 180; #################### subroutine header begin #################### =head2 init Usage : $zd->init Purpose : This is called by the constructor for the Zymonic Decryptor to perform initialisation tasks. Returns : nothing Argument : nothing Throws : Zymonic::Exception::Decryptor::Server::MissingDB Zymonic::Exception::Decryptor::Server::MissingHostname Zymonic::Exception::Decryptor::Server::StartFailed Comment : Currently DB type See Also : Zymonic::Decryptor::new =cut #################### subroutine header end #################### sub init { my $self = shift; $self->SUPER::init; # Load the decryptor DB if ( $self->{config}->{decryptor_db} ) { $self->{decryptor_db} = Zymonic::DB->new( parent => $self, driver_string => $self->{config}->{decryptor_db}, config => $self->{config}, dbusername => $self->{config}->{decryptor_dbusername}, dbpassword => $self->{config}->{decryptor_dbpassword}, ); } else { $self->{decryptor_db} = $self->{db}; } # Exception if no DB Zymonic::Exception::Decryptor::Client::MissingDB->throw() unless $self->{decryptor_db}; # load enc data table object # Load the decryptor log table $self->{ed_table} = $self->get_table( $self->{ed_table_name}, { ident => '', DB => ( $self->{config}->{encrypted_data_store} and $self->{config}->{encrypted_data_store} eq 'decryptor' ) ? $self->{decryptor_db} : $self->{db}, } ); $self->{dead_servers} = []; $self->{timeout} = $self->{config}->{decryptor_connection_timeout} || 60; # set ENV flag to avoid SSL hostname verification for the calls we make $ENV{'PERL_LWP_SSL_VERIFY_HOSTNAME'} = 0; } #################### subroutine header begin #################### =head2 call_decryptor Usage : my $key = $zd->call_decryptor($message, 'all decryptors', 'no key'); Purpose : This sends a message via the decryptor. Returns : the response from the decryptor Argument : the message to send via the decryptor an 'decryptors' parameter, all or a port number. a 'no key' flag a hostname list (array ref) - can include % as wildcard. Throws : Zymonic::Exception::Decryptor::Client::NoDecryptor Zymonic::Exception::Decryptor::Client::InvalidResponse Comment : See Also : =cut #################### subroutine header end #################### sub call_decryptor { my $self = shift; my $message = shift; my $ports = shift || ''; my $no_key = shift || ''; my $hosts = shift || []; my $sdecryptor = shift || ''; my $ret = {}; my $zzfnlevel = debug_function_start($self); # Add the system name to the message $message->{system} = $self->{config}->{system_name} unless ( $self->{config}->{encrypted_data_store} and $self->{config}->{encrypted_data_store} eq 'decryptor' ); my $single_host = 0; my $retry = ''; my @decryptors_to_call = (); if ( ref($sdecryptor) ) { @decryptors_to_call = ($sdecryptor); } elsif ( $ports eq 'all' ) { my @decryptors = $self->get_decryptors( 0, $no_key, $hosts ); Zymonic::Exception::Decryptor::Client::NoDecryptor->throw() unless scalar(@decryptors); @decryptors_to_call = @decryptors; } elsif ( $self->{config}->sys_opt('default_decryptor_host') ) { @decryptors_to_call = ( { hostname => $self->{config}->sys_opt('default_decryptor_host'), id => '' } ); $single_host = 1; } else { my $decryptor = $self->get_decryptors( 0, $no_key, $hosts, $ports ); Zymonic::Exception::Decryptor::Client::NoDecryptor->throw() unless $decryptor; @decryptors_to_call = ($decryptor); $retry = 'true'; } my $last_decryptor; foreach my $decryptor (@decryptors_to_call) { $last_decryptor = $decryptor; eval { $ret = $self->send( $decryptor, $message ); 1; } or do { my $exception = $@; if ($exception) { if ( !$single_host and ref($exception) and $exception->isa('Zymonic::Exception::Decryptor::Client::ConnectFailed') ) { # attempt cleanup push( @{ $self->{dead_servers} }, $decryptor->{id} ); my $response = $self->call_decryptor( { messagetype => 'CleanUp', remove_dead => 'true' } ); # make sure we cleaned up this decryptor if ( ref($response) eq 'HASH' && ref( $response->{dead_servers} ) eq 'ARRAY' && grep { $_->{id} eq $decryptor->{id} } @{ $response->{dead_servers} } ) { # try again if we need to on a different decryptor if ($retry) { debug_function_stop( $self, $zzfnlevel ); return $self->call_decryptor( $message, $ports, $no_key, $hosts ); } } else { # we didn't clean up this decryptor, so its some other problem, rethrow exception rethrow_exception($exception); } } else { rethrow_exception($exception); } } }; } # check if response is valid Zymonic::Exception::Decryptor::Client::InvalidResponse->throw( response => Dumper($ret), decryptor => $last_decryptor ) unless ref($ret) eq 'HASH' && keys %{$ret} > 0; Zymonic::Exception::Decryptor::Client::ErrorResponse->throw( response => $ret->{DSO_error}, decryptor => $last_decryptor ) if $ret->{DSO_error}; debug_function_stop( $self, $zzfnlevel ); return $ret; } #################### subroutine header begin #################### =head2 get_decryptors Usage : my $decryptor = $self->get_decryptors(1,'',[]); Purpose : This returns the next available decryptor. Returns : a hashref containing details of the decryptor. Argument : how many decryptors to get (0 for all), get decryptors without a key flag, hosts to get decryptors on (array ref) - empty for all hosts. decryptor_port - empty for least busy. Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub get_decryptors { my $self = shift; my $count = shift || 0; my $no_key = shift || ''; my $hosts = shift || []; my $port = shift || ''; my @clauses = (); my @clause_params = (); unless ($no_key) { push( @clauses, "has_key = 'Y'" ); } if ( ( ref($hosts) eq 'ARRAY' ) and ( scalar( @{$hosts} ) > 0 ) ) { push( @clauses, "hostname IN(" . join( ",", map { "?" } @{$hosts} ) . ")" ); push( @clause_params, @{$hosts} ); } if ($port) { push( @clauses, "port = ?" ); push( @clause_params, $port ); } if ( ref( $self->{dead_servers} ) eq 'ARRAY' && @{ $self->{dead_servers} } > 0 ) { push( @clauses, 'id NOT IN (' . join( ', ', map { '?' } @{ $self->{dead_servers} } ) . ')' ); push( @clause_params, @{ $self->{dead_servers} } ); } my $decryptors = $self->{decryptor_db}->run_query( { string => 'SELECT ' . $self->{zz_decryptor_fields} . ' FROM zz_decryptors ' . ( scalar(@clauses) ? 'WHERE (' . join( " ) AND ( ", @clauses ) . ")" : '' ) . ' ORDER BY in_use ASC, zzlu ASC ' . ( $count ? 'LIMIT ' . $count : '' ), params => [@clause_params], } ); return ( ref($decryptors) eq 'ARRAY' and scalar( @{$decryptors} ) > 0 ) ? ( wantarray ? @{$decryptors} : $decryptors->[0] ) : ''; } #################### subroutine header begin #################### =head2 send Usage : my $res = $self->send($decryptor, $message) Purpose : This stores some information in a 'memory table'. Returns : the response from the decryptor Argument : a hashref of decryptor information a hashref of information to send Throws : nothing Comment : Notice that we force array on XML Simple and then convert any non-array elements back. This worked around a number of inconsistencies that occurred when converting back and forth. See Also : =cut #################### subroutine header end #################### sub send { my $self = shift; my $decryptor = shift; my $message = shift; my $user_agent = Zymonic::UA->new( ( $decryptor->{fingerprint} ? ( ssl_opts => { SSL_fingerprint => $decryptor->{fingerprint} } ) : undef ) ); $user_agent->timeout( $self->{timeout} ); my $decryptor_url = $self->{config}->sys_opt('decryptor_protocol') . '://' . $decryptor->{hostname} . $self->{config}->sys_opt('decryptor_path'); debug( "Sending to decryptor : " . $decryptor_url, 'error' ); my $req = POST $decryptor_url, Content_Type => 'form-data', Content => [ 'message' => encode_json($message), 'ZZsystem' => $self->{config}->{system_name} ]; my $response = $user_agent->request($req); unless ( $response->is_success ) { # What should we do about system errors coming from the decryptor? Zymonic::Exception::Decryptor::Client::ErrorResponse->throw( response => $response->decoded_content(), decryptor => $decryptor_url, ) if ( $response->decoded_content() =~ /fault has occurred/ ); debug( "Connection failed: " . $response->message() . " (" . $response->code() . ")" ); Zymonic::Exception::Decryptor::Client::ConnectFailed->throw( decryptor => $decryptor_url, status_code => $response->code(), message => $response->message(), ); } return decode_json( $response->decoded_content() ); } #################### subroutine header begin #################### =head2 encrypt_card_number Usage : my $result = $self->encrypt_card_number($card_number, $attempt_find, $extras, $extra_keys); Purpose : Encrypts a card number. Returns : a hashref with a reference number (encrypted_ref) and a masked card number (first 6 digits + last 4) Argument : a card number, a flag indicating whether an attempt should be made to find the card number in the DB, any extra fields to store against the card number and any extra keys to use when finding the ID. Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub encrypt_card_number { my $self = shift; my $card_number = shift; my $attempt_find = shift || 0; my $extras = shift || {}; my $extra_keys = shift || {}; my $result = {}; # Strip spaces only if appears to be a card number $card_number =~ s/\s*//g if $card_number =~ /^[\s0-9]*$/; my $data_length = length($card_number); # if value to encrypt is less than 13 chars, i.e. not a card number, then mask all of it if ( $data_length < 13 ) { $extras->{masked_card_number} = $extra_keys->{masked_card_number} = "X" x $data_length; } else { # Add the masked card number to extras $extras->{masked_card_number} = $extra_keys->{masked_card_number} = substr( $card_number, 0, 6 ) . ( "X" x ( $data_length - 10 ) ) . substr( $card_number, -4 ); } if ( $attempt_find and !$result->{encrypted_ref} ) { # Send an a find id request to the server $result = $self->call_decryptor( { messagetype => 'FindId', extra_keys => $extra_keys, data => $card_number, } ); $result->{masked_card_number} = $extras->{masked_card_number} if $result->{encrypted_ref}; } unless ( $result->{encrypted_ref} ) { # Encrypt it... $result = $self->encrypt( $card_number, $extras ); } return $result; } #################### subroutine header begin #################### =head2 encrypt Usage : my $result = $self->encrypt($data, $extras); Purpose : Encrypts and stores data Returns : a hashref with a reference number (encrypted_ref) and the extras. Argument : a string to encrypt any extra fields to store against the card number Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub encrypt { my $self = shift; my $data = shift; my $extras = shift; my $db = ( $self->{config}->{encrypted_data_store} and $self->{config}->{encrypted_data_store} eq 'decryptor' ) ? $self->{decryptor_db} : $self->{db}; # encrypt the data my $encrypted = encode_base64( $self->encrsa($db)->encrypt($data) ); # store in the DB $self->{ed_table}->insert( { encrypted_data => { value => $encrypted }, keyversion => { value => $self->{keyversion} }, map { $_ => { value => $extras->{$_} } } keys %{$extras} } ); return { encrypted_ref => $db->last_insert_id( $self->{ed_table} ), %{$extras}, }; } #################### subroutine header begin #################### =head2 encrsa Usage : my $encrypted = $self->encrsa()->encrypt($data); Purpose : Returns an RSA encryption object with the current key. Returns : a n RSA encryption object Argument : DB object where data is to be stored. Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub encrsa { my $self = shift; my $db = shift; my $keyversion_rec = $db->run_query( { string => 'SELECT option_value FROM zz_system_options WHERE option_name = \'keyversion\'', params => [], } ); $self->{keyversion} = $keyversion_rec->[0]->{option_value}; unless ( ref( $self->{ 'encrsa' . $self->{keyversion} } ) ) { # Load the public key my $public_key = $self->load_key_file( 'public', $self->{config}->{keydir}, $self->{keyversion} ); # Load the encryptor $self->{ 'encrsa' . $self->{keyversion} } = Crypt::OpenSSL::RSA->new_public_key($public_key); $self->{ 'encrsa' . $self->{keyversion} }->use_pkcs1_oaep_padding(); } return $self->{ 'encrsa' . $self->{keyversion} }; } #################### subroutine header begin #################### =head2 remove_encrypted_data Usage : my $result = $self->remove_encrypted_data($encrypted_ref, $extras); Purpose : Securly removes encrypted data from the decryptor Returns : nothing Argument : a string reference to the encrypted data any extra fields to lookup the data using Throws : Zymonic::Exception::Decryptor::Client Comment : See Also : =cut #################### subroutine header end #################### sub remove_encrypted_data { my $self = shift; my $encrypted_ref = shift; my $extras = shift || {}; my $db = ( ( $self->{config}->{encrypted_data_store} && $self->{config}->{encrypted_data_store} eq 'decryptor' ) ? $self->{decryptor_db} : $self->{db} ); # secure delete the data $db->secure_delete( $self->{ed_table_name}, [ 'zz_df_encrypted_data', 'zz_df_masked_number' ], { clause => 'id = ?' . ( keys %{$extras} > 0 ? ' AND ' . join( ' AND ', map { $_ . ' = ?' } keys %{$extras} ) : '' ), params => [ $encrypted_ref, ( keys %{$extras} > 0 ? values %{$extras} : () ) ] } ); } #################### subroutine header begin #################### =head2 clear_encrypted_data_fields Usage : $self->clear_encrypted_data_fields($fields, $where); Purpose : Clears the fields with the given where clause. Returns : hashref contain id and extras, or undef if no match Argument : encrypted id, list of extra fields, system Throws : Comment : See Also : =cut #################### subroutine header end #################### sub clear_encrypted_data_fields { my $self = shift; my $fields = shift; my $where = shift; # log it $self->connection_log( 'Clearing encrypted data fields (' . join( ', ', @{$fields} ) . ") where '$where->{clause}' (PARAMS: " . join( ', ', @{ $where->{params} } ) . ')' ); my $db = ( ( $self->{config}->{encrypted_data_store} && $self->{config}->{encrypted_data_store} eq 'decryptor' ) ? $self->{decryptor_db} : $self->{db} ); # secure delete the data debug( "Clearing decryptor encrypted data fields: " . Dumper($fields) ); $db->secure_delete( $self->{ed_table_name}, $fields, $where, 'no_delete' ); } #################### subroutine header begin #################### =head2 connection_log Usage : $zdc->connection_log('log message') Purpose : Adds information to the log from this client, i.e. it will have no server informatio in it. Returns : nothing Argument : a message Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub connection_log { my $self = shift; my $message = shift; return unless $message; $self->{log_table} = $self->get_table( 'zz_decryptor_log', { ident => '', DB => $self->{config}->{DB}, } ) unless $self->{log_table}; $self->{log_table}->insert( { start_time => { value => $self->{db}->timestamp_function(), direct_sql => 'true' }, misc_info => { value => $message } } ); } #################### subroutine header begin #################### =head2 store_in_mem Usage : $zdc->store_in_mem('DATA') Purpose : Sends information to the decryptor to store in memory data must not contain ### Returns : a numeric reference to the data (store_ref) and the server (server) Argument : data (scalar), time limit to store the data for (seconds defaults to 1800) Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub store_in_mem { my $self = shift; my $store_val = shift; my $time_lim = shift || 1800; Zymonic::Exception::Decryptor::Client::SIMData->throw() if $store_val =~ /###/; my $stored_key = $self->call_decryptor( { messagetype => 'store_in_mem', timeout => $time_lim, store_val => $store_val } ); return $stored_key; } 1;