#################### main pod documentation begin ################### =head1 NAME Zymonic::Decryptor - Zymonic Decryptor base module. =head1 SYNOPSIS These methods form the basis of a Decryptor. =head1 DESCRIPTION These methods form the basis of a Decryptor. =head1 USAGE The following configuration options need setting in the XML of the Decryptor's system: decryptor_queue_size - Number of active connections to allow per decryptor port (defaults to 10 if not set) decryptor_ssl_key_file - SSL Key File decryptor_ssl_cert_file - SSL Certificate File decryptor_start_port - The lowest port to listen on (defaults to 9599) decrypt_systems - a comma separated list of Zymonic systems to store encrypted data in (optional - if blank only the decryptor's own system will be used.) The following configuration options need setting in the XML of each system being used to store encrypted data: keydir - The directory containing the public key and encrypted private key. enc_db_username - a user to read from the zz_enc_data table as. (optional) enc_db_password - a password for the user above. (mandatory if username specified.) =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::Server; 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; use Zymonic::DB; use Zymonic::Table; use Zymonic::Utils qw(zymonic_log rethrow_exception death_handler random_string debug); use Crypt::OpenSSL::RSA; use Crypt::CBC; use File::Copy; use IO::Socket::SSL; use MIME::Base64; use Net::hostent; use Zymonic; use Exception::Class ( 'Zymonic::Exception::Decryptor' => { isa => 'Zymonic::Exception', fields => ['decryptor'], description => 'Decryptor related exception', }, 'Zymonic::Exception::Decryptor::Server' => { isa => 'Zymonic::Exception::Decryptor', fields => [], description => 'Decryptor Server related exception' }, 'Zymonic::Exception::Decryptor::Server::MissingDB' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => [], description => 'Decryptor Server requires DB' }, 'Zymonic::Exception::Decryptor::Server::MissingHostname' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => [], description => 'Decryptor Server requires hostname' }, 'Zymonic::Exception::Decryptor::Server::StartFailed' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => [], description => 'Decryptor Server failed to start' }, 'Zymonic::Exception::Decryptor::Server::MemKeyNotFound' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => [], description => 'Decryptor Server failed to find the Memory information' }, 'Zymonic::Exception::Decryptor::Server::InvalidSystem' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => [], description => 'Invalid system specified.' }, 'Zymonic::Exception::Decryptor::Server::NoKey' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => [], description => 'Decryptor Server does not have a key loaded yet.' }, 'Zymonic::Exception::Decryptor::Server::DecryptFailed' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => ['encrypted_id'], description => 'Decryptor Server could not decrypt retrieved data.' }, 'Zymonic::Exception::Decryptor::Server::DataNotFound' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => ['encrypted_id'], description => 'Decryptor Server could not find encrypted data' }, 'Zymonic::Exception::Decryptor::Server::RoundTripFailed' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => [], description => 'Decryptor Server could not encrypt and decrypt data with keys.' }, 'Zymonic::Exception::Decryptor::Server::DecryptFailed' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => ['encrypted_id'], description => 'Decryptor Server could not decrypt retrieved data.' }, 'Zymonic::Exception::Decryptor::Server::ForkingFailed' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => [], description => 'Could not Fork.' }, 'Zymonic::Exception::Decryptor::Server::ReEncryptFailed' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => [], description => 'Could not get Data to Reencrypt' }, 'Zymonic::Exception::Decryptor::Server::KeyLength' => { isa => 'Zymonic::Exception::Decryptor::Server', fields => ['key_length'], description => 'Key Length needs to be an integer > 48: [[key_length]]' }, ); #################### 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 : See Also : Zymonic::Decryptor::new =cut #################### subroutine header end #################### sub init { my $self = shift; $self->SUPER::init; # Exception if no DB Zymonic::Exception::Decryptor::Server::MissingDB->throw() unless $self->{db}; # Exception if no hostname Zymonic::Exception::Decryptor::Server::MissingHostname->throw() unless $self->{hostname}; # remove any dead servers now $self->remove_dead_servers(); my $error = ''; foreach my $count ( 1 .. 10 ) { # Get all the current servers my $servers = $self->{db}->run_query( { string => 'SELECT ' . $self->{zz_decryptor_fields} . ' FROM zz_decryptors WHERE hostname = ?', params => [ $self->{hostname} ], } ); # Use the next highest port available (or 9599 if we're first server) $self->{port} = $self->{config}->{decryptor_start_port} || 9599; if ( ref($servers) eq 'ARRAY' and scalar( @{$servers} ) > 0 ) { my $port = ( sort { $b->{port} <=> $a->{port} } @{$servers} )[0]->{port}; $self->{port} = ( $port + 1 ) if $port; } # Write out our record to DB and check it worked $self->{decryptors_table} = $self->get_table( 'zz_decryptors', { DB => $self->{config}->{DB}, } ) unless ref( $self->{decryptors_table} ); $self->{decryptors_table}->insert( { port => { value => $self->{port} }, hostname => { value => $self->{hostname} }, d_posix_process_id => { value => $$ }, in_use => { value => 0 }, } ); my $server_rec = $self->{db}->run_query( { string => 'SELECT ' . $self->{zz_decryptor_fields} . ' FROM zz_decryptors WHERE port = ? AND hostname = ? AND d_posix_process_id = ?', params => [ $self->{port}, $self->{hostname}, $$ ], } ); next unless ref($server_rec) eq 'ARRAY' and scalar( @{$server_rec} ) == 1; # Start the server. $self->{server} = IO::Socket::SSL->new( LocalPort => $self->{port}, Reuse => 1, Listen => $self->{config}->{decryptor_queue_size} || 10, Proto => 'tcp', SSL_verify_mode => 0x00, SSL_key_file => $self->{config}->{decryptor_ssl_key_file}, SSL_cert_file => $self->{config}->{decryptor_ssl_cert_file}, ); last if $self->{server}; sleep(1); } # Success? If not exception. Zymonic::Exception::Decryptor::Server::StartFailed->throw( error => 'Unable to start server: ' . &IO::Socket::SSL::errstr, ) unless $self->{server}; $self->load_systems; # Empty hash for system dbs (if not present). $self->{systems} = {} unless ref( $self->{systems} ); # Load the decryptor log table $self->{log_table} = $self->get_table( 'zz_decryptor_log', { ident => '', DB => $self->{config}->{DB}, } ); # Log start up zymonic_log( $self->{config}, '', 'Decryptor Server on ' . $self->{hostname} . ' port ' . $self->{port} . ' started.', 'Decryptor Server', 'yes', 'information' ); # check for common extra fields $self->{decryptor_extra_fields} = []; if ( $self->{config}->{decryptor_extra_fields} ) { $self->{decryptor_extra_fields} = [ split( /,/, $self->{config}->{decryptor_extra_fields} ) ]; } } #################### subroutine header begin #################### =head2 accept Usage : $zd->accept Purpose : This is just a wrapper around IO::Socket::INET's accept method. Returns : a client connection. Argument : nothing Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub accept { my $self = shift; return $self->{server}->accept(); } #################### subroutine header begin #################### =head2 got_key Usage : $zd->got_key Purpose : Sets the 'has key' flag in the DB, thus allowing clients to connect. Returns : nothing Argument : The system and The keyversion (A or B) Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub got_key { my $self = shift; my $system = shift; my $keyversion = shift; # For each system check which keys are currently in use. my $has_keys = scalar( grep { $self->all_keys_loaded($_) } keys( %{ $self->{systems} } ) ); # Set has_key if all keys in use are loaded. $self->{db}->run_statement( { string => "UPDATE zz_decryptors SET has_key = 'Y' WHERE hostname = ? AND port = ?", params => [ $self->{hostname}, $self->{port} ], } ) if $has_keys; } #################### subroutine header begin #################### =head2 get_from_mem Usage : $zd->get_from_mem Purpose : Gets a 'memory' item. Returns : the item. Argument : the key. Throws : Zymonic::Exception::Decryptor::Server::MemKeyNotFound Comment : See Also : Zymonic::Decryptor::Client::store_in_mem =cut #################### subroutine header end #################### sub get_from_mem { my $self = shift; my $key = shift; # Expire 'old' items. $self->{db}->run_statement( { string => 'DELETE FROM zz_mks WHERE timeout < ?', params => [ time() ], } ); # Try and get the requested key. my $mem_item = $self->{db}->run_query( { string => 'SELECT data FROM zz_mks WHERE pk = ?', params => [$key], } ); Zymonic::Exception::Decryptor::Server::MemKeyNotFound->throw() unless ref($mem_item) eq 'ARRAY' and scalar( @{$mem_item} ) == 1; return $mem_item->[0]->{data}; } #################### subroutine header begin #################### =head2 connection_start Usage : $zd->connection_start('message_type','client_ip') Purpose : Flags the decryptor as in use and logs the starting of the connection. Returns : nothing Argument : nothing Throws : nothing Comment : See Also : Zymonic::Decryptor::Server::connection_close =cut #################### subroutine header end #################### sub connection_start { my $self = shift; my $client_ip = shift; $self->{db}->run_statement( { string => 'UPDATE zz_decryptors SET in_use = in_use + 1 WHERE hostname = ? AND port = ?', params => [ $self->{hostname}, $self->{port} ], } ); $self->{memory_log} = { port => { value => $self->{port} }, hostname => { value => $self->{hostname} }, d_posix_process_id => { value => $$ }, client => { value => $client_ip }, start_time => { value => $self->{db}->timestamp_function(), direct_sql => 'true' }, messages => [], }; # clear encrypted data cache for each connection $self->{encrypted_data_cache} = {}; $self->{log_id} = time() . random_string( 5, ( "a" .. "z" ) ); return $self->{log_id}; } #################### subroutine header begin #################### =head2 connection_log Usage : $zd->connection_log('log message') Purpose : Adds information to the log for the connection. Returns : nothing Argument : a message Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub connection_log { my $self = shift; my $message = shift() . "\n"; my $log_id = shift || $self->{log_id}; my @messages = (); push( @{ $self->{memory_log}->{messages} }, $self->{db}->timestamp( '', 'display_only' ) . " - " . $message ); } #################### subroutine header begin #################### =head2 connection_close Usage : $zd->connection_close('log message') Purpose : Marks the decryptor as available again and logs the close time of the connection. Returns : nothing Argument : nothing Throws : nothing Comment : See Also : Zymonic::Decryptor::Server::connection_start =cut #################### subroutine header end #################### sub connection_close { my $self = shift; my $log_id = shift || $self->{log_id}; my $message = join( "\n", @{ $self->{memory_log}->{messages} } ); $self->{memory_log}->{misc_info} = { value => $message }; delete $self->{memory_log}->{messages}; $self->{log_table}->insert( $self->{memory_log} ); $self->{db}->run_statement( { string => 'UPDATE zz_decryptors SET in_use = in_use - 1 ' . ' WHERE hostname = ? AND port = ?', params => [ $self->{hostname}, $self->{port} ], } ); } #################### subroutine header begin #################### =head2 shutdown Usage : $zd->shutdown Purpose : Removes the decryptor from the table of running decryptors. Returns : nothing Argument : nothing Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub shutdown { my $self = shift; $self->{db}->run_statement( { string => 'DELETE FROM zz_decryptors WHERE hostname = ? AND port = ?', params => [ $self->{hostname}, $self->{port} ], } ); } #################### subroutine header begin #################### =head2 receive Usage : my $incoming = $zd->receive($handle) Purpose : Takes a connection handle, accepts data from it and returns the received hashref having replaced the encrypted references in all scalar values. Returns : a hashref Argument : a handle Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub receive { my $self = shift; my $handle = shift; my $in = $self->unpack_and_thaw($handle); # Add system if not peresent. $in->{system} = $self->{config}->{system_name} unless $in->{system}; # replace references my $ret = {}; eval { $ret = $self->replace_refs( $in, $in->{system} || '' ); 1; } or do { my $exception = $@; if ( ref($exception) and $exception->isa('Zymonic::Exception::Decryptor::Server::NoKey') ) { $self->connection_log('Replacement failed - no key.'); return { message => 'Decryptor has no Key.' }; } elsif ( ref($exception) and $exception->isa('Zymonic::Exception::Decryptor::Server::DataNotFound') ) { $self->connection_log( 'Replacement failed - not found (id ' . $exception->encrypted_id() . ')' ); return { message => 'Encrypted Data not Found' }; } elsif ( ref($exception) and $exception->isa('Zymonic::Exception::Decryptor::Server::DecryptFailed') ) { $self->connection_log( 'Replacement failed - decrypt failed (id ' . $exception->encrypted_id() . ')' ); return { message => 'Encrypted Data not decrypted' }; } elsif ( ref($exception) and $exception->isa('Zymonic::Exception::Decryptor::Server::InvalidSystem') ) { $self->connection_log('Replacement failed - system not recognised.'); return { message => 'System not Recognised' }; } else { rethrow_exception($exception); } }; return $ret; } #################### subroutine header begin #################### =head2 replace_refs Usage : my $out = $zd->replace_refs($in) Purpose : Replaces all memory and encrypted references with the correct values. Returns : a hashref, scalar or arrayref Argument : a hashref, scalar or arrayref Throws : Zymonic::Exception::Decryptor::Server::InvalidSystem Comment : See Also : =cut #################### subroutine header end #################### sub replace_refs { my $self = shift; my $in = shift; my $system = shift || $self->{config}->{system_name}; Zymonic::Exception::Decryptor::Server::InvalidSystem->throw() unless ref( $self->{systems}->{$system} ); if ( ref($in) eq 'HASH' ) { # Call ourself on each key foreach my $key ( keys( %{$in} ) ) { $in->{$key} = $self->replace_refs( $in->{$key}, $system ); } } elsif ( ref($in) eq 'ARRAY' ) { # Call ourself on each value foreach my $i ( 0 .. $#{$in} ) { $in->[$i] = $self->replace_refs( $in->[$i], $system ); } } elsif ( defined $in ) { # Replace the encrypted references and extra lookup references if ( my @refs = $in =~ /ENCRYPTED([0-9]+?(?:\.[0-9a-zA-Z_]+?)?)ENCRYPTED/gm ) { # parse look into hashref { id1 => { extra1 => 1, extra2 => 1, ... }, ... } my $id_extras = {}; foreach my $eid_extras (@refs) { my ( $eid, $extra ) = split( /\./, $eid_extras, 2 ); if ($extra) { $id_extras->{$eid} = {} unless $id_extras->{$eid}; $id_extras->{$eid}->{$extra} = 1; } else { $id_extras->{$eid} = {} unless $id_extras->{$eid}; $id_extras->{$eid}->{encrypted_data} = 1; $id_extras->{$eid}->{keyversion} = 1; } } # iterate over ids, lookup extras and do the replacements for each extra foreach my $eid ( keys %{$id_extras} ) { # cache the lookup so we only do it onces unless ( $self->{encrypted_data_cache}->{$eid} ) { $self->{encrypted_data_cache}->{$eid} = $self->lookup_encrypted_data_record( $eid, [ keys %{ $id_extras->{$eid} } ], $system ); } my $eid_record = $self->{encrypted_data_cache}->{$eid}; # do any decryption replacements if ( $eid_record->{encrypted_data} && $eid_record->{keyversion} ) { my $decrypted_data = $self->decrypt_encrypted_data( $eid_record->{encrypted_data}, $eid_record->{keyversion}, $system ); my $rstring = 'ENCRYPTED' . $eid . 'ENCRYPTED'; $in =~ s/$rstring/$decrypted_data/mg; } # do any extra field replacements map { my $rstring = 'ENCRYPTED' . $eid . '.' . $_ . 'ENCRYPTED'; $in =~ s/$rstring/$eid_record->{$_}/mg; } grep { $_ !~ /encrypted_data|keyversion/ } keys %{ $id_extras->{$eid} }; } } # TODO Replace the memory references } return $in; } #################### subroutine header begin #################### =head2 decrypt Usage : my $decrypted = $zd->decrypt($id) Purpose : Returns the decrypted version of an encrypted piece of data. Returns : a scalar Argument : an id Throws : Zymonic::Exception::Decryptor::Server::NoKey Zymonic::Exception::Decryptor::Server::MissingDB Zymonic::Exception::Decryptor::Server::DecryptFailed Zymonic::Exception::Decryptor::Server::DataNotFound Comment : See Also : =cut #################### subroutine header end #################### sub decrypt { my $self = shift; my $id = shift; my $system = shift; my $return = shift; Zymonic::Exception::Decryptor::Server::MissingDB->throw( error => 'Cannot access encrypted data database.' ) unless ref( $self->{systems}->{$system}->{db} ); my $enc_data = $self->{systems}->{$system}->{db}->run_query( { string => 'SELECT encrypted_data, keyversion FROM zz_enc_data WHERE id = ?', params => [$id], } ); if ( ref($enc_data) eq 'ARRAY' and scalar( @{$enc_data} ) == 1 ) { Zymonic::Exception::Decryptor::Server::NoKey->throw() unless ref( $self->{systems}->{$system}->{decrsa} ) and ref( $self->{systems}->{$system}->{'decrsa'}->{ $enc_data->[0]->{keyversion} } ); # Attempt decryption eval { $return = $self->{systems}->{$system}->{'decrsa'}->{ $enc_data->[0]->{keyversion} } ->decrypt( decode_base64( $enc_data->[0]->{encrypted_data} ) ); 1; } or do { Zymonic::Exception::Decryptor::Server::DecryptFailed->throw( error => $@, encrypted_id => $id, ); }; } else { # Throw not found exception Zymonic::Exception::Decryptor::Server::DataNotFound->throw( encrypted_id => $id ); } return $return; } #################### subroutine header begin #################### =head2 decrypt_encrypted_data Usage : my $decrypted = $zd->decrypt_encrypted_data($encrypted_data, $keyversion) Purpose : Returns the decrypted version of an encrypted piece of data. Returns : a scalar Argument : encrypted data Throws : Zymonic::Exception::Decryptor::Server::NoKey Zymonic::Exception::Decryptor::Server::DecryptFailed Comment : See Also : =cut #################### subroutine header end #################### sub decrypt_encrypted_data { my $self = shift; my $encrypted_data = shift; my $keyversion = shift; my $system = shift; my $return; Zymonic::Exception::Decryptor::Server::NoKey->throw() unless ref( $self->{systems}->{$system}->{decrsa} ) and ref( $self->{systems}->{$system}->{decrsa}->{$keyversion} ); # Attempt decryption eval { $return = $self->{systems}->{$system}->{decrsa}->{$keyversion}->decrypt( decode_base64($encrypted_data) ); 1; } or do { Zymonic::Exception::Decryptor::Server::DecryptFailed->throw( error => $@, ); }; return $return; } #################### subroutine header begin #################### =head2 load_systems Usage : $zd->load_systems() Purpose : Loads the config, private and public keys into memory. Returns : nothing Argument : nothing Throws : nothing Comment : With a large number of systems and a decryptor that forks, having the whole system def would be very wasteful - could add a method that removes the system definition after loading?? See Also : =cut #################### subroutine header end #################### sub load_systems { my $self = shift; my @systems = @_; @systems = ( $self->{config}->{system_name}, split( /,/, $self->{config}->{decrypt_systems} || '' ) ) unless scalar(@systems); $self->{systems} = {} unless ref( $self->{systems} ); foreach my $system (@systems) { eval { # Load completely if not just trying to load keys. unless ( ref( $self->{systems}->{$system} ) ) { $self->{systems}->{$system} = { decrsa => {}, public => {}, enc_private => {}, }; if ( $system eq $self->{config}->{system_name} ) { $self->{systems}->{$system}->{config} = $self->{config}; } else { # Load the config $self->{systems}->{$system}->{config} = Zymonic::Config->new( parent => $self, system_name => $system, config_dir => $self->{config}->{config_dir}, ); } $self->{systems}->{$system}->{config}->load_system_definition; $self->{systems}->{$system}->{config}->load; } foreach my $key (qw(a b)) { if ( $self->{systems}->{$system}->{config}->sys_opt( 'has_key_' . $key ) and $self->{systems}->{$system}->{config}->sys_opt( 'has_key_' . $key ) eq 'YES' ) { # Load the public key $self->{systems}->{$system}->{public}->{$key} = $self->load_key_file( 'public', $self->{systems}->{$system}->{config}->{keydir}, $key ); # Load the private key $self->{systems}->{$system}->{enc_private}->{$key} = $self->load_key_file( 'private', $self->{systems}->{$system}->{config}->{keydir}, $key ); } } 1; } or do { my $exception = $@; if ($exception) { # Cause death handler to record the error, but continue... death_handler( $exception, '', 'return_error' ); } } } } #################### subroutine header begin #################### =head2 complete_system_load Usage : $zd->complete_system_load($system) Purpose : Attempts to decrypt the private key and make the encrypted DB connection if specified. Returns : nothing Argument : system name Throws : Zymonic::Exception::Decryptor::Server::RoundTripFailed Various 'dies' from Crypt modules. Comment : See Also : =cut #################### subroutine header end #################### sub complete_system_load { my $self = shift; my $system = shift; my $keyversion = shift; # Encrypt a fake number my $encrsa = Crypt::OpenSSL::RSA->new_public_key( $self->{systems}->{$system}->{public}->{$keyversion} ); $encrsa->use_pkcs1_oaep_padding(); my $encrypted = encode_base64( $encrsa->encrypt('4111111111111111') ); # Decrypt the private key my $cipher = Crypt::CBC->new( -key => $self->{systems}->{$system}->{KEK}->{$keyversion}, -cipher => 'OpenSSL::AES', ); my $decrypted_key = $cipher->decrypt( $self->{systems}->{$system}->{enc_private}->{$keyversion} ); # Decrypt my $decrsa = Crypt::OpenSSL::RSA->new_private_key($decrypted_key); # Test the result Zymonic::Exception::Decryptor::Server::RoundTripFailed->throw() unless ( $decrsa->decrypt( decode_base64($encrypted) ) eq '4111111111111111' ); # Save the new decryption object $self->{systems}->{$system}->{decrsa}->{$keyversion} = $decrsa; $self->{systems}->{$system}->{encrsa}->{$keyversion} = $encrsa; # if encdb is specified in config, decrypt the password # make and store connection if ( $self->{systems}->{$system}->{config}->{enc_db_username} ) { $self->{systems}->{$system}->{db} = Zymonic::DB->new( parent => $self, config => $self->{systems}->{$system}->{config}, dbusername => $self->{systems}->{$system}->{config}->{enc_db_username}, dbpassword => $self->{systems}->{$system}->{config}->{enc_db_password}, ); } else { # otherwise use main DB. $self->{systems}->{$system}->{db} = $self->{systems}->{$system}->{config}->{DB}; } # If we're here and the keyversion is a 'new' key then # unset our nofork flag. my $nkv = $self->{systems}->{$system}->{config}->sys_opt('new_keyversion'); if ( $nkv and $nkv eq $keyversion ) { $self->{db}->run_statement( { string => "UPDATE zz_decryptors SET nofork = 'N'", params => [], } ); } $self->got_key( $system, $keyversion ); } #################### subroutine header begin #################### =head2 all_keys_loaded Usage : $zd->all_keys_loaded($system) Purpose : Checks which keys are being used in the DB and if they are loaded in this instance of the decryptor Returns : nothing Argument : The system Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub all_keys_loaded { my $self = shift; my $system = shift; # Which keys are in use my $in_use_keys = $self->{systems}->{$system}->{db}->run_query( { string => 'SELECT keyversion FROM zz_enc_data GROUP BY keyversion', params => [], } ); my $ok = 0; if ( ref($in_use_keys) and ref($in_use_keys) eq 'ARRAY' ) { $ok = ( ( # Actual keys scalar( @{$in_use_keys} ) == # Loaded keys scalar( grep { defined( $self->{systems}->{$system}->{decrsa}->{ $_->{keyversion} } ) } @{$in_use_keys} ) ) ? 1 : 0 ); } return $ok; } #################### subroutine header begin #################### =head2 nofork Usage : $zd->nofork Purpose : Returns whether the decryptor is in nofork mode. Returns : whether decryptor is in nofork mode. Argument : nothing Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub nofork { my $self = shift; my $nofork = 0; my $decryptor_rec = $self->{db}->run_query( { string => 'SELECT has_key, nofork FROM zz_decryptors WHERE hostname = ? AND port = ?', params => [ $self->{hostname}, $self->{port} ], } ); $nofork = 1 unless ( $decryptor_rec->[0]->{has_key} and $decryptor_rec->[0]->{has_key} eq 'Y' ); $nofork = 1 if ( $decryptor_rec->[0]->{nofork} and $decryptor_rec->[0]->{nofork} eq 'Y' ); return $nofork; } #################### subroutine header begin #################### =head2 new_keyversion Usage : $zd->new_keyversion($system) Purpose : Returns the 'version' of the new key. Returns : the 'version' of the new key. Argument : the system Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub new_keyversion { my $self = shift; my $system = shift || $self->{config}->{system_name}; my $nkv = $self->{systems}->{$system}->{config}->sys_opt('new_keyversion'); $nkv = $self->generate_new_key($system) unless $nkv and ref( $self->{new_kek_parts} ); # Initiate non-forking mode $self->{db}->run_statement( { string => "UPDATE zz_decryptors SET nofork = 'Y'", params => [], } ); return $nkv; } #################### subroutine header begin #################### =head2 get_kek_part Usage : $zd->get_kek_part($system, $part) Purpose : Returns the 'version' of the new key. Returns : the key part or 'ALREADY SENT' Argument : the system, the part required. Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub get_kek_part { my $self = shift; my $system = shift || $self->{config}->{system_name}; my $part = shift || 1; my $nkv = $self->new_keyversion($system); my $kek_part = $self->{new_kek_parts}->{$part}->{retrieved} ? 'ALREADY SENT' : $self->{new_kek_parts}->{$part}->{content}; $self->{new_kek_parts}->{$part}->{retrieved} = 1; return $kek_part; } #################### subroutine header begin #################### =head2 generate_new_key Usage : $zd->generate_new_key($system) Purpose : Creates a new key Returns : the 'version' of the new key. Argument : the system Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub generate_new_key { my $self = shift; my $system = shift || $self->{config}->{system_name}; # Work out which should be the new key my $current_key = $self->{systems}->{$system}->{config}->sys_opt('keyversion'); my $nkv = ( $current_key and ( $current_key eq 'b' ) ) ? 'a' : 'b'; # Generate the new RSA Key my $key_length = $self->key_length($system); my $rsa = Crypt::OpenSSL::RSA->generate_key($key_length); my $private_key = $rsa->get_private_key_string(); my $public_key = $rsa->get_public_key_string(); # Generate KEK my @chars = ( ( 'A' .. 'Z' ), ( 'a' .. 'z' ), ( 0 .. 9 ) ); my $kek = join( '', map { $chars[ rand( scalar(@chars) ) ] } ( 0 .. 19 ) ); # Encrypt the private key my $cipher = Crypt::CBC->new( -key => $kek, -cipher => 'OpenSSL::AES' ); my $encrypted_key = $cipher->encrypt($private_key); # Split the KEK my @kek_parts = unpack( "a10a10", $kek ); # If keys files already exist, archive them my $private_key_file = $self->{systems}->{$system}->{config}->{keydir} . "/privatekey${nkv}.base64"; my $public_key_file = $self->{systems}->{$system}->{config}->{keydir} . "/publickey${nkv}.txt"; my $archive_dir = $self->{systems}->{$system}->{config}->{keydir} . '/old_keys_' . time(); if ( -f $private_key_file || -f $public_key_file ) { # chmod the main dir so we can create the new archive dir chmod 0755, $self->{systems}->{$system}->{config}->{keydir}; mkdir $archive_dir; move( $private_key_file, $archive_dir . "/privatekey${nkv}.base64" ) if -f $private_key_file; move( $public_key_file, $archive_dir . "/publickey${nkv}.txt" ) if -f $public_key_file; } # Store the keys open( PRV, ">", $private_key_file ); print PRV encode_base64($encrypted_key); close(PRV); open( PUB, ">", $public_key_file ); print PUB $public_key; close(PUB); $self->{systems}->{$system}->{public}->{$nkv} = $public_key; # set file permissions to read-only on new key files and read/execute on dir chmod 0555, $self->{systems}->{$system}->{config}->{keydir}; chmod 0444, $private_key_file, $public_key_file; # Store KEK Parts in memory $self->{new_kek_parts} = { '1' => { content => $kek_parts[0], retrieved => 0 }, '2' => { content => $kek_parts[1], retrieved => 0 }, }; $self->{systems}->{$system}->{config}->sys_opt( 'new_keyversion', $nkv ); $self->{systems}->{$system}->{config}->sys_opt( 'has_key_' . $nkv, 'YES' ); return $nkv; } #################### subroutine header begin #################### =head2 key_length Usage : $zd->key_length($system) Purpose : Returns key length for the system Returns : tkey length Argument : the system Throws : Zymonic::Exception::Decryptor::Server::KeyLength Comment : See Also : =cut #################### subroutine header end #################### sub key_length { my $self = shift; my $system = shift || $self->{config}->{system_name}; # determine key length my $key_length = $self->{systems}->{$system}->{config}->sys_opt( 'DecryptorKeyLength', undef, 'no_cache' ) || $self->{systems}->{$system}->{config}->{decryptor_key_length} || 1024; # docs state this value needs to be an integer greater than 48 # source: http://search.cpan.org/~vipul/Crypt-RSA-1.99/lib/Crypt/RSA/Key.pm#generate() Zymonic::Exception::Decryptor::Server::KeyLength->throw( key_length => $key_length ) unless $key_length =~ /^\d+$/ && $key_length > 48; debug("Key Length: $key_length"); return $key_length; } #################### subroutine header begin #################### =head2 reencrypt Usage : $zd->reencrypt($system) Purpose : Starts or restarts the decryption and re-encryption of stored data. Returns : nothing Argument : the system Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub reencrypt { my $self = shift; my $system = shift || $self->{config}->{system_name}; # Check if there is a new keyversion set my $nkv = $self->{systems}->{$system}->{config}->sys_opt('new_keyversion'); if ($nkv) { # If so fork, swap the keyversion sys opt, record our PID as # the re-encrypt and clear the new version flag $self->{systems}->{$system}->{config}->sys_opt( 'new_keyversion', '' ); my $cpid = fork(); Zymonic::Exception::Decryptor::Server::ForkingFailed->throw() unless defined $cpid; if ($cpid) { $self->postfork($cpid); # Record the PID $self->{systems}->{$system}->{config}->sys_opt( 'reencrypt PID', $cpid ); } else { $self->postfork($cpid); # call decrypt_encrypt and then clear re-encrypt PID. $self->decrypt_encrypt($system); $self->{systems}->{$system}->{config}->sys_opt( 'reencrypt PID', '' ); exit(0); } } else { # Otherwise, check if there is a re-encrypt PID and if the # process is running - if not fork(), call decrypt_encrypt # then clear re-encrypt PID. my $pexists = kill( 0, $self->{systems}->{$system}->{config}->sys_opt('reencrypt PID') ); unless ($pexists) { my $cpid = fork(); Zymonic::Exception::Decryptor::Server::ForkingFailed->throw() unless defined $cpid; if ($cpid) { $self->postfork($cpid); # Record the PID $self->{systems}->{$system}->{config}->sys_opt( 'reencrypt PID', $cpid ); } else { $self->postfork($cpid); # call decrypt_encrypt and then clear re-encrypt PID. $self->decrypt_encrypt($system); $self->{systems}->{$system}->{config}->sys_opt( 'reencrypt PID', '' ); exit(0); } } } } #################### subroutine header begin #################### =head2 decrypt_encrypt Usage : $zd->decrypt_encrypt($system) Purpose : Decrypts and encrypts all data with new key. Returns : nothing Argument : the system Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub decrypt_encrypt { my $self = shift; my $system = shift || $self->{config}->{system_name}; # Get 100 cards encrypted with non-current key my $current_key = $self->{systems}->{$system}->{config}->sys_opt('keyversion'); my $okv = ( $current_key and ( $current_key eq 'b' ) ) ? 'a' : 'b'; my $done = 0; while ( !$done ) { my $enc_data = $self->{systems}->{$system}->{db}->run_query( { string => 'SELECT id, encrypted_data FROM zz_enc_data WHERE keyversion = ? LIMIT 100', params => [$okv], } ); # Decrypt, re-encrypt and then update each. if ( ref($enc_data) and scalar( @{$enc_data} ) ) { foreach my $enc_data ( @{$enc_data} ) { my $data = ''; Zymonic::Exception::Decryptor::Server::NoKey->throw() unless ref( $self->{systems}->{$system}->{decrsa} ) and ref( $self->{systems}->{$system}->{'decrsa'}->{$okv} ); # Attempt decryption eval { $data = $self->{systems}->{$system}->{'decrsa'}->{$okv} ->decrypt( decode_base64( $enc_data->{encrypted_data} ) ); $self->{systems}->{$system}->{db}->run_query( { string => 'UPDATE zz_enc_data SET encrypted_data = ?, keyversion = ? WHERE id = ?', params => [ encode_base64( $self->{systems}->{$system}->{'encrsa'}->{$current_key}->encrypt($data) ), $current_key, $enc_data->{id} ], } ); 1; } or do { Zymonic::Exception::Decryptor::Server::DecryptFailed->throw( error => $@, encrypted_id => $enc_data->{id}, ); }; } } elsif ( ref($enc_data) and ( scalar( @{$enc_data} == 0 ) ) ) { $done = 1; } else { Zymonic::Exception::Decryptor::Server::ReEncryptFailed->throw(); } } } #################### subroutine header begin #################### =head2 encrypted_with_old_key Usage : $zd->encrypted_with_old_key($system) Purpose : Counts how many pieces of data are left to re-encrypt. Returns : an integer Argument : the system Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub encrypted_with_old_key { my $self = shift; my $system = shift || $self->{config}->{system_name}; return undef unless $self->{systems}->{$system}->{db}; # Select count of data encrypted with non-current key. my $current_key = $self->{systems}->{$system}->{config}->sys_opt('keyversion'); my $okv = ( $current_key and ( $current_key eq 'b' ) ) ? 'a' : 'b'; my $old_data = $self->{systems}->{$system}->{db}->run_query( { string => 'SELECT COUNT(id) AS old_key_count FROM zz_enc_data WHERE keyversion = ? LIMIT 100', params => [$okv], } ); return $old_data->[0]->{old_key_count} if ( ref($old_data) and scalar( @{$old_data} ) ); } #################### subroutine header begin #################### =head2 new_key_reset Usage : $zd->new_key_reset($system) Purpose : Clears the new key flag to allow another attempt. Returns : nothing Argument : the system Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub new_key_reset { my $self = shift; my $system = shift || $self->{config}->{system_name}; my $nkv = $self->{systems}->{$system}->{config}->sys_opt('new_keyversion'); $self->{systems}->{$system}->{config}->sys_opt( 'new_keyversion', '' ); $self->{systems}->{$system}->{config}->sys_opt( 'has_key_' . $nkv, '' ); $self->{new_kek_parts} = ''; # Delete DB pass file. unlink( $self->{systems}->{$system}->{config}->{keydir} . "/dbpasswdkey" . $nkv . '.base64' ) if -f $self->{systems}->{$system}->{config}->{keydir} . "/dbpasswdkey" . $nkv . '.base64'; } #################### subroutine header begin #################### =head2 find_id Usage : $zd->find_id($system, $card_number, $extra_keys) Purpose : Clears the new key flag to allow another attempt. Returns : nothing Argument : the system Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub find_id { my $self = shift; my $system = shift || $self->{config}->{system_name}; my $data = shift; my $extra_keys = shift; my $id = ''; # Search on extra keys my $potential_data = $self->{systems}->{$system}->{db}->run_query( { string => 'SELECT id, encrypted_data, keyversion FROM zz_enc_data WHERE ' . join( " AND ", map { $_ . ' = ?' } keys( %{$extra_keys} ) ), params => [ values( %{$extra_keys} ) ] } ); # Decrypt each and test against data if ( ref($potential_data) eq 'ARRAY' ) { foreach my $potential ( @{$potential_data} ) { # Attempt decryption eval { if ( $data eq $self->{systems}->{$system}->{'decrsa'}->{ $potential->{keyversion} } ->decrypt( decode_base64( $potential->{encrypted_data} ) ) ) { $id = $potential->{id}; last; } 1; } or do { Zymonic::Exception::Decryptor::Server::DecryptFailed->throw( error => $@, encrypted_id => $potential->{id}, ); }; } } return $id; } #################### subroutine header begin #################### =head2 has_kek Usage : $zd->has_kek($system, $kek_part, ) Purpose : Returns whether the decryptor has the part of the key encrypting key. Returns : nothing Argument : the system Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub has_kek { my $self = shift; my $system = shift || $self->{config}->{system_name}; my $part = shift || 1; my $nkv = $self->new_keyversion($system); my $has_kek_part = $self->{systems}->{$system}->{ 'kek_part_' . $part }->{$nkv} ? 1 : 0; # Be careful not to accidentally send KEK back if typo is introduced above... $has_kek_part =~ s/[^10]//g; return $has_kek_part; } #################### subroutine header begin #################### =head2 postfork Usage : $zd->postfork Purpose : Does tasks need after forking e.g. database reconnect. Returns : nothing Argument : child pid (if from parent) or 0 if not. Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub postfork { my $self = shift; my $child_pid = shift; if ( defined $child_pid and ( $child_pid == 0 ) ) { # Main DBs $self->{db}->child_mode; $self->{config}->{DB}->child_mode if $self->{db} != $self->{config}->{DB}; # System DBs foreach ( values( %{ $self->{systems} } ) ) { if ( ref( $_->{db} ) and ( $_->{db} != $self->{config}->{DB} ) and ( $_->{db} != $self->{db} ) ) { $_->{db}->child_mode; } } } } #################### subroutine header begin #################### =head2 postfork Usage : $zd->disconnect Purpose : Does a disconnect from all DBs. Returns : nothing Argument : nothing Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub disconnect { my $self = shift; # We undef the connection if its a reused connection otherwise # the final disconnect(s) don't work. # System DBs foreach ( values( %{ $self->{systems} } ) ) { if ( $_->{db} == $self->{config}->{DB} or $_->{db} == $self->{db} ) { $_->{db} = undef; } elsif ( ref( $_->{db} ) ) { $_->{db}->close_db_connection; } } # Main DBs if ( $self->{db} == $self->{config}->{DB} ) { $self->{db} = undef; } else { $self->{db}->close_db_connection; } } #################### subroutine header begin #################### =head2 remove_dead_servers Usage : $self->remove_dead_servers(); Purpose : Checks can ping each active decryptor server, and if not removes its entry fro the decryptors table Returns : list of decryptors that were removed, empty if none Argument : nothing Throws : Comment : See Also : =cut #################### subroutine header end #################### sub remove_dead_servers { my $self = shift; my @dead_decryptors = (); my $decryptors = $self->{db}->run_query( { string => 'SELECT ' . $self->{zz_decryptor_fields} . ' FROM zz_decryptors WHERE hostname = ?', params => [ $self->{hostname} ], } ); if ( ref($decryptors) eq 'ARRAY' && @{$decryptors} > 0 ) { @dead_decryptors = grep { !kill( 0, $_->{d_posix_process_id} ) } @{$decryptors}; if (@dead_decryptors) { $self->{db}->run_query( { string => 'DELETE FROM zz_decryptors WHERE id IN (' . join( ', ', map { '?' } @dead_decryptors ) . ')', params => [ map { $_->{id} } @dead_decryptors ], } ); } } return wantarray ? @dead_decryptors : \@dead_decryptors; } #################### subroutine header begin #################### =head2 lookup_encrypted_data_record Usage : $self->lookup_encrypted_data_record($id, $extras, $system); Purpose : Looks up values of extras for given id 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 lookup_encrypted_data_record { my $self = shift; my $id = shift; my $extra_fields = shift; my $system = shift; my @fields = ( qw(id), @{$extra_fields}, @{ $self->{decryptor_extra_fields} } ); my $data = $self->{systems}->{$system}->{db}->run_query( { string => 'SELECT ' . join( ', ', @fields ) . ' FROM zz_enc_data WHERE id = ?', params => [$id] } ); return $data->[0]; } 1;