diff options
Diffstat (limited to 'lib/GnuPG/Interface.pm')
-rw-r--r-- | lib/GnuPG/Interface.pm | 1484 |
1 files changed, 1484 insertions, 0 deletions
diff --git a/lib/GnuPG/Interface.pm b/lib/GnuPG/Interface.pm new file mode 100644 index 0000000..905ae30 --- /dev/null +++ b/lib/GnuPG/Interface.pm @@ -0,0 +1,1484 @@ +# Interface.pm +# - providing an object-oriented approach to interacting with GnuPG +# +# Copyright (C) 2000 Frank J. Tobin <ftobin@cpan.org> +# +# This module is free software; you can redistribute it and/or modify it +# under the same terms as Perl itself. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# + +package GnuPG::Interface; +use Moo; +use MooX::late; +with qw(GnuPG::HashInit); + +use English qw( -no_match_vars ); +use Carp; +use Fcntl; +use vars qw( $VERSION ); +use Fatal qw( open close pipe fcntl ); +use Class::Struct; +use IO::Handle; + +use Math::BigInt try => 'GMP'; +use GnuPG::Options; +use GnuPG::Handles; +use Scalar::Util 'tainted'; + +$VERSION = '1.02'; + +has passphrase => ( + isa => 'Any', + is => 'rw', + clearer => 'clear_passphrase', +); + +has call => ( + isa => 'Any', + is => 'rw', + trigger => 1, + clearer => 'clear_call', +); + +# NB: GnuPG versions +# +# There are now two supported versions of GnuPG: legacy 1.4 and stable 2.2 +# They are detected and each behave slightly differently. +# +# When using features specific to branches, check that the system's +# version of gpg corresponds to the branch. +# +# legacy: 1.4 +# stable: >= 2.2 +# +# You can find examples of version comparison in the tests. +has version => ( + isa => 'Str', + is => 'ro', + reader => 'version', + writer => '_set_version', +); + +has options => ( + isa => 'GnuPG::Options', + is => 'rw', + lazy_build => 1, +); + +sub _build_options { GnuPG::Options->new() } + +# deprecated! +sub gnupg_call { shift->call(@_); } + +sub BUILD { + my ( $self, $args ) = @_; + $self->hash_init( call => 'gpg', %$args ); +} + +struct( + fh_setup => { + parent_end => '$', child_end => '$', + direct => '$', is_std => '$', + parent_is_source => '$', name_shows_dup => '$', + } +); + +# Update version if "call" is updated +sub _trigger_call { + my ( $self, $gpg ) = @_; + $self->_set_version( $self->_version() ); +} + +################################################################# +# real worker functions + +# This function does any 'extra' stuff that the user might +# not want to handle himself, such as passing in the passphrase +sub wrap_call( $% ) { + my ( $self, %args ) = @_; + + my $handles = $args{handles} + or croak 'error: no handles defined'; + + $handles->stdin('<&STDIN') unless $handles->stdin(); + $handles->stdout('>&STDOUT') unless $handles->stdout(); + $handles->stderr('>&STDERR') unless $handles->stderr(); + + $self->passphrase("\n") unless $self->passphrase(); + + my $needs_passphrase_handled + = ( $self->passphrase() =~ m/\S/ and not $handles->passphrase() ) ? 1 : 0; + + if ($needs_passphrase_handled) { + $handles->passphrase( IO::Handle->new() ); + } + + my $pid = $self->fork_attach_exec(%args); + + if ($needs_passphrase_handled) { + my $passphrase_handle = $handles->passphrase(); + print $passphrase_handle $self->passphrase(); + close $passphrase_handle; + + # We put this in in case the user wants to re-use this object + $handles->clear_passphrase(); + } + + return $pid; +} + +# does does command-line creation, forking, and execcing +# the reasing cli creation is done here is because we should +# fork before finding the fd's for stuff like --status-fd +sub fork_attach_exec( $% ) { + my ( $self, %args ) = @_; + + my $handles = $args{handles} or croak 'no GnuPG::Handles passed'; + my $use_loopback_pinentry = 0; + + # Don't use loopback pintentry for legacy (1.4) GPG + # + # Check that $version is populated before running cmp_version. If + # we are invoked as part of BUILD to populate $version, then any + # methods that depend on $version will fail. We don't care about + # loopback when we're called just to check gpg version. + $use_loopback_pinentry = 1 + if ($handles->passphrase() && $self->version && $self->cmp_version($self->version, '2.2') > 0 ); + + # deprecation support + $args{commands} ||= $args{gnupg_commands}; + + my @commands + = ref $args{commands} ? @{ $args{commands} } : ( $args{commands} ) + or croak "no gnupg commands passed"; + + # deprecation support + $args{command_args} ||= $args{gnupg_command_args}; + + my @command_args + = ref $args{command_args} + ? @{ $args{command_args} } + : ( $args{command_args} || () ); + unshift @command_args, "--" + if @command_args and $command_args[0] ne "--"; + + my %fhs; + foreach my $fh_name ( + qw( stdin stdout stderr status + logger passphrase command + ) + ) { + my $fh = $handles->$fh_name() or next; + $fhs{$fh_name} = fh_setup->new(); + $fhs{$fh_name}->parent_end($fh); + } + + foreach my $fh_name (qw( stdin stdout stderr )) { + $fhs{$fh_name}->is_std(1); + } + + foreach my $fh_name (qw( stdin passphrase command )) { + my $entry = $fhs{$fh_name} or next; + $entry->parent_is_source(1); + } + + # Below is code derived heavily from + # Marc Horowitz's IPC::Open3, a base Perl module + foreach my $fh_name ( keys %fhs ) { + my $entry = $fhs{$fh_name}; + + my $parent_end = $entry->parent_end(); + my $name_shows_dup = ( $parent_end =~ s/^[<>]&// ); + $entry->parent_end($parent_end); + + $entry->name_shows_dup($name_shows_dup); + + $entry->direct( $name_shows_dup + || $handles->options($fh_name)->{direct} + || 0 ); + } + + foreach my $fh_name ( keys %fhs ) { + $fhs{$fh_name}->child_end( IO::Handle->new() ); + } + + foreach my $fh_name ( keys %fhs ) { + my $entry = $fhs{$fh_name}; + next if $entry->direct(); + + my $reader_end; + my $writer_end; + if ( $entry->parent_is_source() ) { + $reader_end = $entry->child_end(); + $writer_end = $entry->parent_end(); + } + else { + $reader_end = $entry->parent_end(); + $writer_end = $entry->child_end(); + } + + pipe $reader_end, $writer_end; + } + + my $pid = fork; + + die "fork failed: $ERRNO" unless defined $pid; + + if ( $pid == 0 ) # child + { + + # these are for safety later to help lessen autovifying, + # speed things up, and make the code smaller + my $stdin = $fhs{stdin}; + my $stdout = $fhs{stdout}; + my $stderr = $fhs{stderr}; + + # Paul Walmsley says: + # Perl 5.6's POSIX.pm has a typo in it that prevents us from + # importing STDERR_FILENO. So we resort to requiring it. + require POSIX; + + my $standard_out + = IO::Handle->new_from_fd( &POSIX::STDOUT_FILENO, 'w' ); + my $standard_in + = IO::Handle->new_from_fd( &POSIX::STDIN_FILENO, 'r' ); + + # Paul Walmsley says: + # this mess is due to a typo in POSIX.pm on Perl 5.6 + my $stderr_fd = eval {&POSIX::STDERR_FILENO}; + $stderr_fd = 2 unless defined $stderr_fd; + my $standard_err = IO::Handle->new_from_fd( $stderr_fd, 'w' ); + + # If she wants to dup the kid's stderr onto her stdout I need to + # save a copy of her stdout before I put something else there. + if ( $stdout->parent_end() ne $stderr->parent_end() + and $stderr->direct() + and my_fileno( $stderr->parent_end() ) + == my_fileno($standard_out) ) { + my $tmp = IO::Handle->new(); + open $tmp, '>&' . my_fileno( $stderr->parent_end() ); + $stderr->parent_end($tmp); + } + + if ( $stdin->direct() ) { + open $standard_in, '<&' . my_fileno( $stdin->parent_end() ) + unless my_fileno($standard_in) + == my_fileno( $stdin->parent_end() ); + } + else { + close $stdin->parent_end(); + open $standard_in, '<&=' . my_fileno( $stdin->child_end() ); + } + + if ( $stdout->direct() ) { + open $standard_out, '>&' . my_fileno( $stdout->parent_end() ) + unless my_fileno($standard_out) + == my_fileno( $stdout->parent_end() ); + } + else { + close $stdout->parent_end(); + open $standard_out, '>&=' . my_fileno( $stdout->child_end() ); + } + + if ( $stdout->parent_end() ne $stderr->parent_end() ) { + + # I have to use a fileno here because in this one case + # I'm doing a dup but the filehandle might be a reference + # (from the special case above). + if ( $stderr->direct() ) { + open $standard_err, '>&' . my_fileno( $stderr->parent_end() ) + unless my_fileno($standard_err) + == my_fileno( $stderr->parent_end() ); + } + else { + close $stderr->parent_end(); + open $standard_err, '>&=' . my_fileno( $stderr->child_end() ); + } + } + else { + open $standard_err, '>&STDOUT' + unless my_fileno($standard_err) == my_fileno($standard_out); + } + + foreach my $fh_name ( keys %fhs ) { + my $entry = $fhs{$fh_name}; + next if $entry->is_std(); + + my $parent_end = $entry->parent_end(); + my $child_end = $entry->child_end(); + + if ( $entry->direct() ) { + if ( $entry->name_shows_dup() ) { + my $open_prefix + = $entry->parent_is_source() ? '<&' : '>&'; + open $child_end, $open_prefix . $parent_end; + } + else { + $child_end = $parent_end; + $entry->child_end($child_end); + } + } + else { + close $parent_end; + } + + # we want these fh's to stay open after the exec + fcntl $child_end, F_SETFD, 0; + + # now set the options for the call to GnuPG + my $fileno = my_fileno($child_end); + my $option = $fh_name . '_fd'; + $self->options->$option($fileno); + } + + my @args = $self->options->get_args(); + + # Get around a bug in 2.2, see also https://dev.gnupg.org/T4667 + # this covers both --delete-secret-key(s) and --delete-secret-and-public-key(s) + if ( $self->version && $self->cmp_version( $self->version, 2.2 ) >= 0 && $commands[0] =~ /^--delete-secret-.*keys?$/ ) { + push @args, '--yes'; + } + + push @args, '--pinentry-mode', 'loopback' + if $use_loopback_pinentry; + + my @command = ( + $self->call(), @args, + @commands, @command_args + ); + + local $ENV{PATH} if tainted $ENV{PATH}; + exec @command or die "exec() error: $ERRNO"; + } + + # parent + + # close the child end of any pipes (non-direct stuff) + foreach my $fh_name ( keys %fhs ) { + my $entry = $fhs{$fh_name}; + close $entry->child_end() unless $entry->direct(); + } + + foreach my $fh_name ( keys %fhs ) { + my $entry = $fhs{$fh_name}; + next unless $entry->parent_is_source(); + + my $parent_end = $entry->parent_end(); + + # close any writing handles if they were a dup + #any real reason for this? It bombs if we're doing + #the automagic >& stuff. + #close $parent_end if $entry->direct(); + + # unbuffer pipes + select( ( select($parent_end), $OUTPUT_AUTOFLUSH = 1 )[0] ) + if $parent_end; + } + + return $pid; +} + +sub my_fileno { + no strict 'refs'; + my ($fh) = @_; + croak "fh is undefined" unless defined $fh; + return $1 if $fh =~ /^=?(\d+)$/; # is it a fd in itself? + my $fileno = fileno $fh; + croak "error determining fileno for $fh: $ERRNO" unless defined $fileno; + return $fileno; +} + + +sub unescape_string { + my($str) = splice(@_); + $str =~ s/\\x(..)/chr(hex($1))/eg; + return $str; +} + +################################################################### + +sub get_public_keys ( $@ ) { + my ( $self, @key_ids ) = @_; + + return $self->get_keys( + commands => ['--list-public-keys'], + command_args => [@key_ids], + ); +} + +sub get_secret_keys ( $@ ) { + my ( $self, @key_ids ) = @_; + + return $self->get_keys( + commands => ['--list-secret-keys'], + command_args => [@key_ids], + ); +} + +sub get_public_keys_with_sigs ( $@ ) { + my ( $self, @key_ids ) = @_; + + return $self->get_keys( + commands => ['--check-sigs'], + command_args => [@key_ids], + ); +} + +sub get_keys { + my ( $self, %args ) = @_; + + my $saved_options = $self->options(); + my $new_options = $self->options->copy(); + $self->options($new_options); + $self->options->push_extra_args( + '--with-colons', + '--fixed-list-mode', + '--with-fingerprint', + '--with-fingerprint', + '--with-key-data', + ); + + my $stdin = IO::Handle->new(); + my $stdout = IO::Handle->new(); + + my $handles = GnuPG::Handles->new( + stdin => $stdin, + stdout => $stdout, + ); + + my $pid = $self->wrap_call( + handles => $handles, + %args, + ); + + my @returned_keys; + my $current_primary_key; + my $current_signed_item; + my $current_key; + + require GnuPG::PublicKey; + require GnuPG::SecretKey; + require GnuPG::SubKey; + require GnuPG::Fingerprint; + require GnuPG::UserId; + require GnuPG::UserAttribute; + require GnuPG::Signature; + require GnuPG::Revoker; + + while (<$stdout>) { + my $line = $_; + chomp $line; + my @fields = split ':', $line, -1; + next unless @fields > 3; + + my $record_type = $fields[0]; + + if ( $record_type eq 'pub' or $record_type eq 'sec' ) { + push @returned_keys, $current_primary_key + if $current_primary_key; + + my ( + $user_id_validity, $key_length, $algo_num, $hex_key_id, + $creation_date, $expiration_date, + $local_id, $owner_trust, $user_id_string, + $sigclass, #unused + $usage_flags, + ) = @fields[ 1 .. $#fields ]; + + # --fixed-list-mode uses epoch time for creation and expiration date strings. + # For backward compatibility, we convert them back using GMT; + my $expiration_date_string; + if ($expiration_date eq '') { + $expiration_date = undef; + } else { + $expiration_date_string = $self->_downrez_date($expiration_date); + } + my $creation_date_string = $self->_downrez_date($creation_date); + + $current_primary_key = $current_key + = $record_type eq 'pub' + ? GnuPG::PublicKey->new() + : GnuPG::SecretKey->new(); + + $current_primary_key->hash_init( + length => $key_length, + algo_num => $algo_num, + hex_id => $hex_key_id, + local_id => $local_id, + owner_trust => $owner_trust, + creation_date => $creation_date, + expiration_date => $expiration_date, + creation_date_string => $creation_date_string, + expiration_date_string => $expiration_date_string, + usage_flags => $usage_flags, + ); + + $current_signed_item = $current_primary_key; + } + elsif ( $record_type eq 'fpr' ) { + my $hex = $fields[9]; + my $f = GnuPG::Fingerprint->new( as_hex_string => $hex ); + $current_key->fingerprint($f); + } + elsif ( $record_type eq 'sig' or + $record_type eq 'rev' + ) { + my ( + $validity, + $algo_num, $hex_key_id, + $signature_date, + $expiration_date, + $user_id_string, + $sig_type, + ) = @fields[ 1, 3 .. 6, 9, 10 ]; + + my $expiration_date_string; + if ($expiration_date eq '') { + $expiration_date = undef; + } else { + $expiration_date_string = $self->_downrez_date($expiration_date); + } + my $signature_date_string = $self->_downrez_date($signature_date); + + my ($sig_class, $is_exportable); + if ($sig_type =~ /^([[:xdigit:]]{2})([xl])$/ ) { + $sig_class = hex($1); + $is_exportable = ('x' eq $2); + } + + my $signature = GnuPG::Signature->new( + validity => $validity, + algo_num => $algo_num, + hex_id => $hex_key_id, + date => $signature_date, + date_string => $signature_date_string, + expiration_date => $expiration_date, + expiration_date_string => $expiration_date_string, + user_id_string => unescape_string($user_id_string), + sig_class => $sig_class, + is_exportable => $is_exportable, + ); + + if ( $current_signed_item->isa('GnuPG::Key') || + $current_signed_item->isa('GnuPG::UserId') || + $current_signed_item->isa('GnuPG::Revoker') || + $current_signed_item->isa('GnuPG::UserAttribute')) { + if ($record_type eq 'sig') { + $current_signed_item->push_signatures($signature); + } elsif ($record_type eq 'rev') { + $current_signed_item->push_revocations($signature); + } + } else { + warn "do not know how to handle signature line: $line\n"; + } + } + elsif ( $record_type eq 'uid' ) { + my ( $validity, $user_id_string ) = @fields[ 1, 9 ]; + + $current_signed_item = GnuPG::UserId->new( + validity => $validity, + as_string => unescape_string($user_id_string), + ); + + $current_primary_key->push_user_ids($current_signed_item); + } + elsif ( $record_type eq 'uat' ) { + my ( $validity, $subpacket ) = @fields[ 1, 9 ]; + + my ( $subpacket_count, $subpacket_total_size ) = split(/ /,$subpacket); + + $current_signed_item = GnuPG::UserAttribute->new( + validity => $validity, + subpacket_count => $subpacket_count, + subpacket_total_size => $subpacket_total_size, + ); + + $current_primary_key->push_user_attributes($current_signed_item); + } + elsif ( $record_type eq 'sub' or $record_type eq 'ssb' ) { + my ( + $validity, $key_length, $algo_num, $hex_id, + $creation_date, $expiration_date, + $local_id, + $dummy0, $dummy1, $dummy2, #unused + $usage_flags, + ) = @fields[ 1 .. 11 ]; + + my $expiration_date_string; + if ($expiration_date eq '') { + $expiration_date = undef; + } else { + $expiration_date_string = $self->_downrez_date($expiration_date); + } + my $creation_date_string = $self->_downrez_date($creation_date); + + $current_signed_item = $current_key + = GnuPG::SubKey->new( + validity => $validity, + length => $key_length, + algo_num => $algo_num, + hex_id => $hex_id, + creation_date => $creation_date, + expiration_date => $expiration_date, + creation_date_string => $creation_date_string, + expiration_date_string => $expiration_date_string, + local_id => $local_id, + usage_flags => $usage_flags, + ); + + $current_primary_key->push_subkeys($current_signed_item); + } + elsif ($record_type eq 'rvk') { + my ($algo_num, $fpr, $class) = @fields[ 3,9,10 ]; + my $rvk = GnuPG::Revoker->new( + fingerprint => GnuPG::Fingerprint->new( as_hex_string => $fpr ), + algo_num => ($algo_num + 0), + class => hex($class), + ); + # pushing to either primary key or subkey, to handle + # designated revokers to the subkeys too: + $current_key->push_revokers($rvk); + # revokers should be bound to the key with signatures: + $current_signed_item = $rvk; + } + elsif ($record_type eq 'pkd') { + my ($pos, $size, $data) = @fields[ 1,2,3 ]; + $current_key->pubkey_data->[$pos+0] = Math::BigInt->from_hex('0x'.$data); + } + elsif ( $record_type ne 'tru' and $record_type ne 'grp' ) { + warn "unknown record type $record_type"; + } + } + + waitpid $pid, 0; + + push @returned_keys, $current_primary_key + if $current_primary_key; + + $self->options($saved_options); + + return @returned_keys; +} + +sub _downrez_date { + my $self = shift; + my $date = shift; + if ($date =~ /^\d+$/) { + my ($year,$month,$day) = (gmtime($date))[5,4,3]; + $year += 1900; + $month += 1; + return sprintf('%04d-%02d-%02d', $year, $month, $day); + } + return $date; +} + + +################################################################ + +sub list_public_keys { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--list-public-keys'], + ); +} + +sub list_sigs { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--list-sigs'], + ); +} + +sub list_secret_keys { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--list-secret-keys'], + ); +} + +sub encrypt( $% ) { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--encrypt'] + ); +} + +sub encrypt_symmetrically( $% ) { + my ( $self, %args ) = @_; + # Strip the homedir and put it back after encrypting; + my $homedir = $self->options->homedir; + $self->options->clear_homedir + unless $self->cmp_version($self->version, '2.2') >= 0; + my $pid = $self->wrap_call( + %args, + commands => ['--symmetric'] + ); + $self->options->homedir($homedir) + unless $self->cmp_version($self->version, '2.2') >= 0; + return $pid; +} + +sub sign( $% ) { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--sign'] + ); +} + +sub clearsign( $% ) { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args,, + commands => ['--clearsign'] + ); +} + +sub detach_sign( $% ) { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--detach-sign'] + ); +} + +sub sign_and_encrypt( $% ) { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => [ + '--sign', + '--encrypt' + ] + ); +} + +sub decrypt( $% ) { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--decrypt'] + ); +} + +sub verify( $% ) { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--verify'] + ); +} + +sub import_keys( $% ) { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--import'] + ); +} + +sub export_keys( $% ) { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--export'] + ); +} + +sub recv_keys( $% ) { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--recv-keys'] + ); +} + +sub send_keys( $% ) { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--send-keys'] + ); +} + +sub search_keys( $% ) { + my ( $self, %args ) = @_; + return $self->wrap_call( + %args, + commands => ['--search-keys'] + ); +} + +sub _version { + my ( $self ) = @_; + + my $out = IO::Handle->new; + my $handles = GnuPG::Handles->new( stdout => $out ); + my $pid = $self->wrap_call( commands => [ '--no-options', '--version' ], handles => $handles ); + my $line = $out->getline; + $line =~ /(\d+\.\d+\.\d+)/; + + my $version = $1; + unless ($self->cmp_version($version, '2.2') >= 0 or + ($self->cmp_version($version, '1.4') >= 0 and $self->cmp_version($version, '1.5') < 0 )) { + croak "GnuPG Version 1.4 or 2.2+ required"; + } + waitpid $pid, 0; + + return $version; +} + +sub cmp_version($$) { + my ( $self, $a, $b ) = (@_); + my @a = split '\.', $a; + my @b = split '\.', $b; + @a > @b + ? push @b, (0) x (@a-@b) + : push @a, (0) x (@b-@a); + for ( my $i = 0; $i < @a; $i++ ) { + return $a[$i] <=> $b[$i] if $a[$i] <=> $b[$i]; + } + return 0; +} + +sub test_default_key_passphrase() { + my ($self) = @_; + + # We can't do something like let the user pass + # in a passphrase handle because we don't exist + # anymore after the user runs off with the + # attachments + croak 'No passphrase defined to test!' + unless defined $self->passphrase(); + + my $stdin = IO::Handle->new(); + my $stdout = IO::Handle->new(); + my $stderr = IO::Handle->new(); + my $status = IO::Handle->new(); + + my $handles = GnuPG::Handles->new( + stdin => $stdin, + stdout => $stdout, + stderr => $stderr, + status => $status + ); + + # save this setting since we need to be in non-interactive mode + my $saved_meta_interactive_option = $self->options->meta_interactive(); + $self->options->clear_meta_interactive(); + + my $pid = $self->sign( handles => $handles ); + + close $stdin; + + # restore this setting to its original setting + $self->options->meta_interactive($saved_meta_interactive_option); + + # all we realy want to check is the status fh + while (<$status>) { + if (/^\[GNUPG:\]\s*(GOOD_PASSPHRASE|SIG_CREATED)/) { + waitpid $pid, 0; + return 1; + } + } + + # If we didn't catch the regexp above, we'll assume + # that the passphrase was incorrect + waitpid $pid, 0; + return 0; +} + +1; + +############################################################## + +=head1 NAME + +GnuPG::Interface - Perl interface to GnuPG + +=head1 SYNOPSIS + + # A simple example + use IO::Handle; + use GnuPG::Interface; + + # setting up the situation + my $gnupg = GnuPG::Interface->new(); + $gnupg->options->hash_init( armor => 1, + homedir => '/home/foobar' ); + + # Note you can set the recipients even if you aren't encrypting! + $gnupg->options->push_recipients( 'ftobin@cpan.org' ); + $gnupg->options->meta_interactive( 0 ); + + # how we create some handles to interact with GnuPG + my $input = IO::Handle->new(); + my $output = IO::Handle->new(); + my $handles = GnuPG::Handles->new( stdin => $input, + stdout => $output ); + + # Now we'll go about encrypting with the options already set + my @plaintext = ( 'foobar' ); + my $pid = $gnupg->encrypt( handles => $handles ); + + # Now we write to the input of GnuPG + print $input @plaintext; + close $input; + + # now we read the output + my @ciphertext = <$output>; + close $output; + + waitpid $pid, 0; + +=head1 DESCRIPTION + +GnuPG::Interface and its associated modules are designed to +provide an object-oriented method for interacting with GnuPG, +being able to perform functions such as but not limited +to encrypting, signing, +decryption, verification, and key-listing parsing. + +=head2 How Data Member Accessor Methods are Created + +Each module in the GnuPG::Interface bundle relies +on Moo to generate the get/set methods +used to set the object's data members. +I<This is very important to realize.> This means that +any data member which is a list has special +methods assigned to it for pushing, popping, and +clearing the list. + +=head2 Understanding Bidirectional Communication + +It is also imperative to realize that this package +uses interprocess communication methods similar to +those used in L<IPC::Open3> +and L<perlipc/"Bidirectional Communication with Another Process">, +and that users of this package +need to understand how to use this method because this package +does not abstract these methods for the user greatly. +This package is not designed +to abstract this away entirely (partly for security purposes), but rather +to simply help create 'proper', clean calls to GnuPG, and to implement +key-listing parsing. +Please see L<perlipc/"Bidirectional Communication with Another Process"> +to learn how to deal with these methods. + +Using this package to do message processing generally +invovlves creating a GnuPG::Interface object, creating +a GnuPG::Handles object, +setting some options in its B<options> data member, +and then calling a method which invokes GnuPG, such as +B<clearsign>. One then interacts with with the handles +appropriately, as described in +L<perlipc/"Bidirectional Communication with Another Process">. + +=head1 GnuPG Versions + +As of this version of GnuPG::Interface, there are two supported +versions of GnuPG: 1.4.x and 2.2.x. The +L<GnuPG download page|https://gnupg.org/download/index.html> has +updated information on the currently supported versions. + +GnuPG released 2.0 and 2.1 versions in the past and some packaging +systems may still provide these if you install the default C<gpg>, +C<gnupg>, C<gnupg2>, etc. packages. This modules supports only +version 2.2.x, so you may need to find additional package +repositories or build from source to get the updated version. + +=head1 OBJECT METHODS + +=head2 Initialization Methods + +=over 4 + +=item new( I<%initialization_args> ) + +This methods creates a new object. The optional arguments are +initialization of data members. + +=item hash_init( I<%args> ). + + +=back + +=head2 Object Methods which use a GnuPG::Handles Object + +=over 4 + +=item list_public_keys( % ) + +=item list_sigs( % ) + +=item list_secret_keys( % ) + +=item encrypt( % ) + +=item encrypt_symmetrically( % ) + +=item sign( % ) + +=item clearsign( % ) + +=item detach_sign( % ) + +=item sign_and_encrypt( % ) + +=item decrypt( % ) + +=item verify( % ) + +=item import_keys( % ) + +=item export_keys( % ) + +=item recv_keys( % ) + +=item send_keys( % ) + +=item search_keys( % ) + +These methods each correspond directly to or are very similar +to a GnuPG command described in L<gpg>. Each of these methods +takes a hash, which currently must contain a key of B<handles> +which has the value of a GnuPG::Handles object. +Another optional key is B<command_args> which should have the value of an +array reference; these arguments will be passed to GnuPG as command arguments. +These command arguments are used for such things as determining the keys to +list in the B<export_keys> method. I<Please note that GnuPG command arguments +are not the same as GnuPG options>. To understand what are options and +what are command arguments please read L<gpg/"COMMANDS"> and L<gpg/"OPTIONS">. + +Each of these calls returns the PID for the resulting GnuPG process. +One can use this PID in a C<waitpid> call instead of a C<wait> call +if more precise process reaping is needed. + +These methods will attach the handles specified in the B<handles> object +to the running GnuPG object, so that bidirectional communication +can be established. That is, the optionally-defined B<stdin>, +B<stdout>, B<stderr>, B<status>, B<logger>, and +B<passphrase> handles will be attached to +GnuPG's input, output, standard error, +the handle created by setting B<status-fd>, the handle created by setting B<logger-fd>, and the handle created by setting +B<passphrase-fd> respectively. +This tying of handles of similar to the process +done in I<IPC::Open3>. + +If you want the GnuPG process to read or write directly to an already-opened +filehandle, you cannot do this via the normal I<IPC::Open3> mechanisms. +In order to accomplish this, set the appropriate B<handles> data member +to the already-opened filehandle, and then set the option B<direct> to be true +for that handle, as described in L<GnuPG::Handles/options>. For example, +to have GnuPG read from the file F<input.txt> and write to F<output.txt>, +the following snippet may do: + + my $infile = IO::File->new( 'input.txt' ); + my $outfile = IO::File->new( '>output.txt' ); + my $handles = GnuPG::Handles->new( stdin => $infile, + stdout => $outfile, + ); + $handles->options( 'stdin' )->{direct} = 1; + $handles->options( 'stdout' )->{direct} = 1; + +If any handle in the B<handles> object is not defined, GnuPG's input, output, +and standard error will be tied to the running program's standard error, +standard output, or standard error. If the B<status> or B<logger> handle +is not defined, this channel of communication is never established with GnuPG, +and so this information is not generated and does not come into play. + +If the B<passphrase> data member handle of the B<handles> object +is not defined, but the the B<passphrase> data member handle of GnuPG::Interface +object is, GnuPG::Interface will handle passing this information into GnuPG +for the user as a convenience. Note that this will result in +GnuPG::Interface storing the passphrase in memory, instead of having +it simply 'pass-through' to GnuPG via a handle. + +If neither the B<passphrase> data member of the GnuPG::Interface nor +the B<passphrase> data member of the B<handles> object is defined, +then GnuPG::Interface assumes that access and control over the secret +key will be handled by the running gpg-agent process. This represents +the simplest mode of operation with the GnuPG "stable" suite (version +2.2 and later). It is also the preferred mode for tools intended to +be user-facing, since the user will be prompted directly by gpg-agent +for use of the secret key material. Note that for programmatic use, +this mode requires the gpg-agent and pinentry to already be correctly +configured. + +=back + +=head2 Other Methods + +=over 4 + +=item get_public_keys( @search_strings ) + +=item get_secret_keys( @search_strings ) + +=item get_public_keys_with_sigs( @search_strings ) + +These methods create and return objects of the type GnuPG::PublicKey +or GnuPG::SecretKey respectively. This is done by parsing the output +of GnuPG with the option B<with-colons> enabled. The objects created +do or do not have signature information stored in them, depending +if the method ends in I<_sigs>; this separation of functionality is there +because of performance hits when listing information with signatures. + +=item test_default_key_passphrase() + +This method will return a true or false value, depending +on whether GnuPG reports a good passphrase was entered +while signing a short message using the values of +the B<passphrase> data member, and the default +key specified in the B<options> data member. + +=item version() + +Returns the version of GnuPG that GnuPG::Interface is running. + +=back + + +=head1 Invoking GnuPG with a custom call + +GnuPG::Interface attempts to cover a lot of the commands +of GnuPG that one would want to perform; however, there may be a lot +more calls that GnuPG is and will be capable of, so a generic command +interface is provided, C<wrap_call>. + +=over 4 + +=item wrap_call( %args ) + +Call GnuPG with a custom command. The %args hash must contain +at least the following keys: + +=over 4 + +=item commands + +The value of this key in the hash must be a reference to a a list of +commands for GnuPG, such as C<[ qw( --encrypt --sign ) ]>. + +=item handles + +As with most other GnuPG::Interface methods, B<handles> +must be a GnuPG::Handles object. + +=back + +The following keys are optional. + +=over 4 + +=item command_args + +As with other GnuPG::Interface methods, the value in hash +for this key must be a reference to a list of arguments +to be passed to the GnuPG command, such as which +keys to list in a key-listing. + +=back + +=back + + +=head1 OBJECT DATA MEMBERS + +=over 4 + +=item call + +This defines the call made to invoke GnuPG. Defaults to 'gpg'; this +should be changed if 'gpg' is not in your path, or there is a different +name for the binary on your system. + +=item passphrase + +In order to lessen the burden of using handles by the user of this package, +setting this option to one's passphrase for a secret key will allow +the package to enter the passphrase via a handle to GnuPG by itself +instead of leaving this to the user. See also L<GnuPG::Handles/passphrase>. + +=item options + +This data member, of the type GnuPG::Options; the setting stored in this +data member are used to determine the options used when calling GnuPG +via I<any> of the object methods described in this package. +See L<GnuPG::Options> for more information. + +=back + +=head1 EXAMPLES + +The following setup can be done before any of the following examples: + + use IO::Handle; + use GnuPG::Interface; + + my @original_plaintext = ( "How do you doo?" ); + my $passphrase = "Three Little Pigs"; + + my $gnupg = GnuPG::Interface->new(); + + $gnupg->options->hash_init( armor => 1, + recipients => [ 'ftobin@uiuc.edu', + '0xABCD1234ABCD1234ABCD1234ABCD1234ABCD1234' ], + meta_interactive => 0 , + ); + + $gnupg->options->debug_level(4); + + $gnupg->options->logger_file("/tmp/gnupg-$$-decrypt-".time().".log"); + + +=head2 Encrypting + + # We'll let the standard error of GnuPG pass through + # to our own standard error, by not creating + # a stderr-part of the $handles object. + my ( $input, $output ) = ( IO::Handle->new(), + IO::Handle->new() ); + + my $handles = GnuPG::Handles->new( stdin => $input, + stdout => $output ); + + # this sets up the communication + # Note that the recipients were specified earlier + # in the 'options' data member of the $gnupg object. + my $pid = $gnupg->encrypt( handles => $handles ); + + # this passes in the plaintext + print $input @original_plaintext; + + # this closes the communication channel, + # indicating we are done + close $input; + + my @ciphertext = <$output>; # reading the output + + waitpid $pid, 0; # clean up the finished GnuPG process + +=head2 Signing + + # This time we'll catch the standard error for our perusing + my ( $input, $output, $error ) = ( IO::Handle->new(), + IO::Handle->new(), + IO::Handle->new(), + ); + + my $handles = GnuPG::Handles->new( stdin => $input, + stdout => $output, + stderr => $error, + ); + + # indicate our pasphrase through the + # convenience method + $gnupg->passphrase( $passphrase ); + + # this sets up the communication + my $pid = $gnupg->sign( handles => $handles ); + + # this passes in the plaintext + print $input @original_plaintext; + + # this closes the communication channel, + # indicating we are done + close $input; + + my @ciphertext = <$output>; # reading the output + my @error_output = <$error>; # reading the error + + close $output; + close $error; + + waitpid $pid, 0; # clean up the finished GnuPG process + +=head2 Decryption + + # This time we'll catch the standard error for our perusing + # as well as passing in the passphrase manually + # as well as the status information given by GnuPG + my ( $input, $output, $error, $passphrase_fh, $status_fh ) + = ( IO::Handle->new(), + IO::Handle->new(), + IO::Handle->new(), + IO::Handle->new(), + IO::Handle->new(), + ); + + my $handles = GnuPG::Handles->new( stdin => $input, + stdout => $output, + stderr => $error, + passphrase => $passphrase_fh, + status => $status_fh, + ); + + # this time we'll also demonstrate decrypting + # a file written to disk + # Make sure you "use IO::File" if you use this module! + my $cipher_file = IO::File->new( 'encrypted.gpg' ); + + # this sets up the communication + my $pid = $gnupg->decrypt( handles => $handles ); + + # This passes in the passphrase + print $passphrase_fh $passphrase; + close $passphrase_fh; + + # this passes in the plaintext + print $input $_ while <$cipher_file>; + + # this closes the communication channel, + # indicating we are done + close $input; + close $cipher_file; + + my @plaintext = <$output>; # reading the output + my @error_output = <$error>; # reading the error + my @status_info = <$status_fh>; # read the status info + + # clean up... + close $output; + close $error; + close $status_fh; + + waitpid $pid, 0; # clean up the finished GnuPG process + +=head2 Printing Keys + + # This time we'll just let GnuPG print to our own output + # and read from our input, because no input is needed! + my $handles = GnuPG::Handles->new(); + + my @ids = ( 'ftobin', '0xABCD1234ABCD1234ABCD1234ABCD1234ABCD1234' ); + + # this time we need to specify something for + # command_args because --list-public-keys takes + # search ids as arguments + my $pid = $gnupg->list_public_keys( handles => $handles, + command_args => [ @ids ] ); + + waitpid $pid, 0; + +=head2 Creating GnuPG::PublicKey Objects + + my @ids = [ 'ftobin', '0xABCD1234ABCD1234ABCD1234ABCD1234ABCD1234' ]; + + my @keys = $gnupg->get_public_keys( @ids ); + + # no wait is required this time; it's handled internally + # since the entire call is encapsulated + +=head2 Custom GnuPG call + + # assuming $handles is a GnuPG::Handles object + my $pid = $gnupg->wrap_call + ( commands => [ qw( --list-packets ) ], + command_args => [ qw( test/key.1.asc ) ], + handles => $handles, + ); + + my @out = <$handles->stdout()>; + waitpid $pid, 0; + + +=head1 FAQ + +=over 4 + +=item How do I get GnuPG::Interface to read/write directly from +a filehandle? + +You need to set GnuPG::Handles B<direct> option to be true for the +filehandles in concern. See L<GnuPG::Handles/options> and +L<"Object Methods which use a GnuPG::Handles Object"> for more +information. + +=item Why do you make it so difficult to get GnuPG to write/read +from a filehandle? In the shell, I can just call GnuPG +with the --outfile option! + +There are lots of issues when trying to tell GnuPG to read/write +directly from a file, such as if the file isn't there, or +there is a file, and you want to write over it! What do you +want to happen then? Having the user of this module handle +these questions beforehand by opening up filehandles to GnuPG +lets the user know fully what is going to happen in these circumstances, +and makes the module less error-prone. + +=item When having GnuPG process a large message, sometimes it just +hanges there. + +Your problem may be due to buffering issues; when GnuPG reads/writes +to B<non-direct> filehandles (those that are sent to filehandles +which you read to from into memory, not that those access the disk), +buffering issues can mess things up. I recommend looking into +L<GnuPG::Handles/options>. + +=back + +=head1 NOTES + +This package is the successor to PGP::GPG::MessageProcessor, +which I found to be too inextensible to carry on further. +A total redesign was needed, and this is the resulting +work. + +After any call to a GnuPG-command method of GnuPG::Interface +in which one passes in the handles, +one should all B<wait> to clean up GnuPG from the process table. + + +=head1 BUGS + +=head2 Large Amounts of Data + +Currently there are problems when transmitting large quantities +of information over handles; I'm guessing this is due +to buffering issues. This bug does not seem specific to this package; +IPC::Open3 also appears affected. + +=head2 OpenPGP v3 Keys + +I don't know yet how well this module handles parsing OpenPGP v3 keys. + +=head2 RHEL 7 Test Failures + +Testing with the updates for version 1.00 we saw intermittent test failures +on RHEL 7 with GnuPG version 2.2.20. In some cases the tests would all pass +for several runs, then one would fail. We're unable to reliably reproduce +this so we would be interested in feedback from other users. + +=head1 SEE ALSO + +L<GnuPG::Options>, +L<GnuPG::Handles>, +L<GnuPG::PublicKey>, +L<GnuPG::SecretKey>, +L<gpg>, +L<perlipc/"Bidirectional Communication with Another Process"> + +=head1 LICENSE + +This module is free software; you can redistribute it and/or modify it +under the same terms as Perl itself. + +=head1 AUTHOR + +GnuPG::Interface is currently maintained by Best Practical Solutions <BPS@cpan.org>. + +Frank J. Tobin, ftobin@cpan.org was the original author of the package. + +=cut + +1; + |