X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FCrypt%2FGnuPG.pm;h=6164a4241ed083d7500f1d2980051762d183edb8;hb=7588a4ac90a9b07c08a3107cd1107d773be1c991;hp=5581df15339402a6372149893b1eb42015b47eff;hpb=b4b0c7e72d7eaee2fbfc7022022c9698323203dd;p=freeside.git diff --git a/rt/lib/RT/Crypt/GnuPG.pm b/rt/lib/RT/Crypt/GnuPG.pm index 5581df153..6164a4241 100644 --- a/rt/lib/RT/Crypt/GnuPG.pm +++ b/rt/lib/RT/Crypt/GnuPG.pm @@ -1,40 +1,40 @@ # BEGIN BPS TAGGED BLOCK {{{ -# +# # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC -# -# +# +# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC +# +# # (Except where explicitly superseded by other copyright notices) -# -# +# +# # LICENSE: -# +# # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from www.gnu.org. -# +# # This work 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. See the GNU # General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 or visit their web page on the internet at # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. -# -# +# +# # CONTRIBUTION SUBMISSION POLICY: -# +# # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of # the GNU General Public License and is only of importance to you if # you choose to contribute your changes and enhancements to the # community by submitting them to Best Practical Solutions, LLC.) -# +# # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with # Request Tracker, to Best Practical Solutions, LLC, you confirm that @@ -43,7 +43,7 @@ # royalty-free, perpetual, license to use, copy, create derivative # works based on those contributions, and sublicense and distribute # those contributions and any derivatives thereof. -# +# # END BPS TAGGED BLOCK }}} use strict; @@ -54,7 +54,7 @@ package RT::Crypt::GnuPG; use IO::Handle; use GnuPG::Interface; use RT::EmailParser (); -use RT::Util 'safe_run_child'; +use RT::Util 'safe_run_child', 'mime_recommended_filename'; =head1 NAME @@ -168,7 +168,7 @@ quoted, otherwise you can see quite cryptic error 'gpg: Invalid option "--0"'. =item --homedir -The GnuPG home directory, by default it is set to F. +The GnuPG home directory, by default it is set to F. You can manage this data with the 'gpg' commandline utility using the GNUPGHOME environment variable or --homedir option. @@ -351,14 +351,16 @@ my %supported_opt = map { $_ => 1 } qw( verbose ); +our $RE_FILE_EXTENSIONS = qr/pgp|asc/i; + # DEV WARNING: always pass all STD* handles to GnuPG interface even if we don't -# need them, just pass 'new IO::Handle' and then close it after safe_run_child. +# need them, just pass 'IO::Handle->new()' and then close it after safe_run_child. # we don't want to leak anything into FCGI/Apache/MP handles, this break things. # So code should look like: # my $handles = GnuPG::Handles->new( -# stdin => ($handle{'stdin'} = new IO::Handle), -# stdout => ($handle{'stdout'} = new IO::Handle), -# stderr => ($handle{'stderr'} = new IO::Handle), +# stdin => ($handle{'stdin'} = IO::Handle->new()), +# stdout => ($handle{'stdout'} = IO::Handle->new()), +# stderr => ($handle{'stderr'} = IO::Handle->new()), # ... # ); @@ -433,7 +435,7 @@ sub SignEncryptRFC3156 { @_ ); - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); # handling passphrase in GnuPGOptions @@ -615,7 +617,7 @@ sub _SignEncryptTextInline { ); return unless $args{'Sign'} || $args{'Encrypt'}; - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); # handling passphrase in GnupGOptions @@ -682,7 +684,7 @@ sub _SignEncryptTextInline { return %res; } - $entity->bodyhandle( new MIME::Body::File $tmp_fn ); + $entity->bodyhandle( MIME::Body::File->new( $tmp_fn) ); $entity->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh; return %res; @@ -703,7 +705,7 @@ sub _SignEncryptAttachmentInline { ); return unless $args{'Sign'} || $args{'Encrypt'}; - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); # handling passphrase in GnupGOptions @@ -769,7 +771,7 @@ sub _SignEncryptAttachmentInline { return %res; } - my $filename = $entity->head->recommended_filename || 'no_name'; + my $filename = mime_recommended_filename( $entity ) || 'no_name'; if ( $args{'Sign'} && !$args{'Encrypt'} ) { $entity->make_multipart; $entity->attach( @@ -779,7 +781,7 @@ sub _SignEncryptAttachmentInline { Disposition => 'attachment', ); } else { - $entity->bodyhandle( new MIME::Body::File $tmp_fn ); + $entity->bodyhandle(MIME::Body::File->new( $tmp_fn) ); $entity->effective_type('application/octet-stream'); $entity->head->mime_attr( $_ => "$filename.pgp" ) foreach (qw(Content-Type.name Content-Disposition.filename)); @@ -805,7 +807,7 @@ sub SignEncryptContent { ); return unless $args{'Sign'} || $args{'Encrypt'}; - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); # handling passphrase in GnupGOptions @@ -891,19 +893,48 @@ sub FindProtectedParts { # inline PGP block, only in singlepart unless ( $entity->is_multipart ) { + my $file = ($entity->head->recommended_filename||'') =~ /\.${RE_FILE_EXTENSIONS}$/; + my $io = $entity->open('r'); unless ( $io ) { $RT::Logger->warning( "Entity of type ". $entity->effective_type ." has no body" ); return (); } + + # Deal with "partitioned" PGP mail, which (contrary to common + # sense) unnecessarily applies a base64 transfer encoding to PGP + # mail (whose content is already base64-encoded). + if ( $entity->bodyhandle->is_encoded and $entity->head->mime_encoding ) { + my $decoder = MIME::Decoder->new( $entity->head->mime_encoding ); + if ($decoder) { + local $@; + eval { + my $buf = ''; + open my $fh, '>', \$buf + or die "Couldn't open scalar for writing: $!"; + binmode $fh, ":raw"; + $decoder->decode($io, $fh); + close $fh or die "Couldn't close scalar: $!"; + + open $fh, '<', \$buf + or die "Couldn't re-open scalar for reading: $!"; + binmode $fh, ":raw"; + $io = $fh; + 1; + } or do { + $RT::Logger->error("Couldn't decode body: $@"); + } + } + } + while ( defined($_ = $io->getline) ) { next unless /^-----BEGIN PGP (SIGNED )?MESSAGE-----/; my $type = $1? 'signed': 'encrypted'; $RT::Logger->debug("Found $type inline part"); return { Type => $type, - Format => 'Inline', - Data => $entity, + Format => !$file || $type eq 'signed'? 'Inline' : 'Attachment', + Data => $entity, }; } $io->close; @@ -1000,7 +1031,7 @@ sub FindProtectedParts { # attachments with inline encryption my @encrypted_indices = - grep {($entity->parts($_)->head->recommended_filename || '') =~ /\.pgp$/} + grep {($entity->parts($_)->head->recommended_filename || '') =~ /\.${RE_FILE_EXTENSIONS}$/} 0 .. $entity->parts - 1; foreach my $i ( @encrypted_indices ) { @@ -1026,46 +1057,70 @@ sub FindProtectedParts { =cut sub VerifyDecrypt { - my %args = ( Entity => undef, Detach => 1, SetStatus => 1, @_ ); + my %args = ( + Entity => undef, + Detach => 1, + SetStatus => 1, + AddStatus => 0, + @_ + ); my @protected = FindProtectedParts( Entity => $args{'Entity'} ); my @res; # XXX: detaching may brake nested signatures foreach my $item( grep $_->{'Type'} eq 'signed', @protected ) { + my $status_on; if ( $item->{'Format'} eq 'RFC3156' ) { push @res, { VerifyRFC3156( %$item, SetStatus => $args{'SetStatus'} ) }; if ( $args{'Detach'} ) { $item->{'Top'}->parts( [ $item->{'Data'} ] ); $item->{'Top'}->make_singlepart; } - $item->{'Top'}->head->set( 'X-RT-GnuPG-Status' => $res[-1]->{'status'} ) - if $args{'SetStatus'}; + $status_on = $item->{'Top'}; } elsif ( $item->{'Format'} eq 'Inline' ) { push @res, { VerifyInline( %$item ) }; - $item->{'Data'}->head->set( 'X-RT-GnuPG-Status' => $res[-1]->{'status'} ) - if $args{'SetStatus'}; + $status_on = $item->{'Data'}; } elsif ( $item->{'Format'} eq 'Attachment' ) { push @res, { VerifyAttachment( %$item ) }; if ( $args{'Detach'} ) { - $item->{'Top'}->parts( [ grep "$_" ne $item->{'Signature'}, $item->{'Top'}->parts ] ); + $item->{'Top'}->parts( [ + grep "$_" ne $item->{'Signature'}, $item->{'Top'}->parts + ] ); $item->{'Top'}->make_singlepart; } - $item->{'Data'}->head->set( 'X-RT-GnuPG-Status' => $res[-1]->{'status'} ) - if $args{'SetStatus'}; + $status_on = $item->{'Data'}; + } + if ( $args{'SetStatus'} || $args{'AddStatus'} ) { + my $method = $args{'AddStatus'} ? 'add' : 'set'; + # Let the header be modified so continuations are handled + my $modify = $status_on->head->modify; + $status_on->head->modify(1); + $status_on->head->$method( + 'X-RT-GnuPG-Status' => $res[-1]->{'status'} + ); + $status_on->head->modify($modify); } } foreach my $item( grep $_->{'Type'} eq 'encrypted', @protected ) { + my $status_on; if ( $item->{'Format'} eq 'RFC3156' ) { push @res, { DecryptRFC3156( %$item ) }; - $item->{'Top'}->head->set( 'X-RT-GnuPG-Status' => $res[-1]->{'status'} ) - if $args{'SetStatus'}; + $status_on = $item->{'Top'}; } elsif ( $item->{'Format'} eq 'Inline' ) { push @res, { DecryptInline( %$item ) }; - $item->{'Data'}->head->set( 'X-RT-GnuPG-Status' => $res[-1]->{'status'} ) - if $args{'SetStatus'}; + $status_on = $item->{'Data'}; } elsif ( $item->{'Format'} eq 'Attachment' ) { push @res, { DecryptAttachment( %$item ) }; - $item->{'Data'}->head->set( 'X-RT-GnuPG-Status' => $res[-1]->{'status'} ) - if $args{'SetStatus'}; + $status_on = $item->{'Data'}; + } + if ( $args{'SetStatus'} || $args{'AddStatus'} ) { + my $method = $args{'AddStatus'} ? 'add' : 'set'; + # Let the header be modified so continuations are handled + my $modify = $status_on->head->modify; + $status_on->head->modify(1); + $status_on->head->$method( + 'X-RT-GnuPG-Status' => $res[-1]->{'status'} + ); + $status_on->head->modify($modify); } } return @res; @@ -1076,7 +1131,7 @@ sub VerifyInline { return DecryptInline( @_ ) } sub VerifyAttachment { my %args = ( Data => undef, Signature => undef, Top => undef, @_ ); - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); $opt{'digest-algo'} ||= 'SHA1'; $gnupg->options->hash_init( @@ -1130,7 +1185,7 @@ sub VerifyAttachment { sub VerifyRFC3156 { my %args = ( Data => undef, Signature => undef, Top => undef, @_ ); - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); $opt{'digest-algo'} ||= 'SHA1'; $gnupg->options->hash_init( @@ -1183,7 +1238,7 @@ sub DecryptRFC3156 { @_ ); - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); # handling passphrase in GnupGOptions @@ -1245,7 +1300,7 @@ sub DecryptRFC3156 { } seek $tmp_fh, 0, 0; - my $parser = new RT::EmailParser; + my $parser = RT::EmailParser->new(); my $decrypted = $parser->ParseMIMEEntityFromFileHandle( $tmp_fh, 0 ); $decrypted->{'__store_link_to_object_to_avoid_early_cleanup'} = $parser; $args{'Top'}->parts( [] ); @@ -1261,7 +1316,7 @@ sub DecryptInline { @_ ); - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); # handling passphrase in GnuPGOptions @@ -1290,11 +1345,12 @@ sub DecryptInline { die "Entity has no body, never should happen"; } + my %res; + my ($had_literal, $in_block) = ('', 0); my ($block_fh, $block_fn) = File::Temp::tempfile( UNLINK => 1 ); binmode $block_fh, ':raw'; - my %res; while ( defined(my $str = $io->getline) ) { if ( $in_block && $str =~ /^-----END PGP (?:MESSAGE|SIGNATURE)-----/ ) { print $block_fh $str; @@ -1335,8 +1391,28 @@ sub DecryptInline { } $io->close; + if ( $in_block ) { + # we're still in a block, this not bad not good. let's try to + # decrypt what we have, it can be just missing -----END PGP... + seek $block_fh, 0, 0; + + my ($res_fh, $res_fn); + ($res_fh, $res_fn, %res) = _DecryptInlineBlock( + %args, + GnuPG => $gnupg, + BlockHandle => $block_fh, + ); + return %res unless $res_fh; + + print $tmp_fh "-----BEGIN OF PGP PROTECTED PART-----\n" if $had_literal; + while (my $buf = <$res_fh> ) { + print $tmp_fh $buf; + } + print $tmp_fh "-----END OF PART-----\n" if $had_literal; + } + seek $tmp_fh, 0, 0; - $args{'Data'}->bodyhandle( new MIME::Body::File $tmp_fn ); + $args{'Data'}->bodyhandle(MIME::Body::File->new( $tmp_fn )); $args{'Data'}->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh; return %res; } @@ -1399,7 +1475,7 @@ sub DecryptAttachment { @_ ); - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); # handling passphrase in GnuPGOptions @@ -1432,12 +1508,19 @@ sub DecryptAttachment { ); return %res unless $res_fh; - $args{'Data'}->bodyhandle( new MIME::Body::File $res_fn ); + $args{'Data'}->bodyhandle(MIME::Body::File->new($res_fn) ); $args{'Data'}->{'__store_tmp_handle_to_avoid_early_cleanup'} = $res_fh; - my $filename = $args{'Data'}->head->recommended_filename; - $filename =~ s/\.pgp$//i; - $args{'Data'}->head->mime_attr( $_ => $filename ) + my $head = $args{'Data'}->head; + + # we can not trust original content type + # TODO: and don't have way to detect, so we just use octet-stream + # some clients may send .asc files (encryped) as text/plain + $head->mime_attr( "Content-Type" => 'application/octet-stream' ); + + my $filename = $head->recommended_filename; + $filename =~ s/\.${RE_FILE_EXTENSIONS}$//i; + $head->mime_attr( $_ => $filename ) foreach (qw(Content-Type.name Content-Disposition.filename)); return %res; @@ -1450,7 +1533,7 @@ sub DecryptContent { @_ ); - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); # handling passphrase in GnupGOptions @@ -1560,8 +1643,7 @@ User friendly message. =back -This parser is based on information from GnuPG distribution, see also -F in the RT distribution. +This parser is based on information from GnuPG distribution. =cut @@ -1636,6 +1718,7 @@ my %ignore_keyword = map { $_ => 1 } qw( BEGIN_ENCRYPTION SIG_ID VALIDSIG ENC_TO BEGIN_DECRYPTION END_DECRYPTION GOODMDC TRUST_UNDEFINED TRUST_NEVER TRUST_MARGINAL TRUST_FULLY TRUST_ULTIMATE + DECRYPTION_INFO ); sub ParseStatus { @@ -1963,7 +2046,7 @@ sub CheckRecipients { # good, one suitable and trusted key next; } - my $user = RT::User->new( $RT::SystemUser ); + my $user = RT::User->new( RT->SystemUser ); $user->LoadByEmail( $address ); # it's possible that we have no User record with the email $user = undef unless $user->id; @@ -2039,7 +2122,7 @@ sub GetKeysInfo { return (exit_code => 0) unless $force; } - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); $opt{'digest-algo'} ||= 'SHA1'; $opt{'with-colons'} = undef; # parseable format @@ -2059,7 +2142,9 @@ sub GetKeysInfo { eval { local $SIG{'CHLD'} = 'DEFAULT'; my $method = $type eq 'private'? 'list_secret_keys': 'list_public_keys'; - my $pid = safe_run_child { $gnupg->$method( handles => $handles, $email? (command_args => $email) : () ) }; + my $pid = safe_run_child { $gnupg->$method( handles => $handles, $email + ? (command_args => [ "--", $email]) + : () ) }; close $handle{'stdin'}; waitpid $pid, 0; }; @@ -2225,7 +2310,7 @@ sub _ParseDate { return $value unless $value; require RT::Date; - my $obj = RT::Date->new( $RT::SystemUser ); + my $obj = RT::Date->new( RT->SystemUser ); # unix time if ( $value =~ /^\d+$/ ) { $obj->Set( Value => $value ); @@ -2238,7 +2323,7 @@ sub _ParseDate { sub DeleteKey { my $key = shift; - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); $gnupg->options->hash_init( _PrepareGnuPGOptions( %opt ), @@ -2250,11 +2335,10 @@ sub DeleteKey { eval { local $SIG{'CHLD'} = 'DEFAULT'; - local @ENV{'LANG', 'LC_ALL'} = ('C', 'C'); my $pid = safe_run_child { $gnupg->wrap_call( handles => $handles, commands => ['--delete-secret-and-public-key'], - command_args => [$key], + command_args => ["--", $key], ) }; close $handle{'stdin'}; while ( my $str = readline $handle{'status'} ) { @@ -2286,7 +2370,7 @@ sub DeleteKey { sub ImportKey { my $key = shift; - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); $gnupg->options->hash_init( _PrepareGnuPGOptions( %opt ), @@ -2298,7 +2382,6 @@ sub ImportKey { eval { local $SIG{'CHLD'} = 'DEFAULT'; - local @ENV{'LANG', 'LC_ALL'} = ('C', 'C'); my $pid = safe_run_child { $gnupg->wrap_call( handles => $handles, commands => ['--import'], @@ -2369,7 +2452,7 @@ properly (and false otherwise). sub Probe { - my $gnupg = new GnuPG::Interface; + my $gnupg = GnuPG::Interface->new(); my %opt = RT->Config->Get('GnuPGOptions'); $gnupg->options->hash_init( _PrepareGnuPGOptions( %opt ), @@ -2399,11 +2482,17 @@ sub Probe { # it's general error system error or incorrect command, command is correct, # but there is no way to get actuall error if ( $? && ($? >> 8) != 2 ) { - $RT::Logger->debug( - "Probe for GPG failed." + my $msg = "Probe for GPG failed." ." Process exitted with code ". ($? >> 8) . ($? & 127 ? (" as recieved signal ". ($? & 127)) : '') - ); + . "."; + foreach ( qw(stderr logger status) ) { + my $tmp = do { local $/; readline $handle{$_} }; + next unless $tmp && $tmp =~ /\S/s; + close $handle{$_}; + $msg .= "\n$_:\n$tmp\n"; + } + $RT::Logger->debug( $msg ); return 0; } return 1; @@ -2411,30 +2500,16 @@ sub Probe { sub _make_gpg_handles { - my %handle_map = ( - stdin => IO::Handle->new(), - stdout => IO::Handle->new(), - stderr => IO::Handle->new(), - logger => IO::Handle->new(), - status => IO::Handle->new(), - command => IO::Handle->new(), - - - @_); + my %handle_map = (@_); + $handle_map{$_} = IO::Handle->new + foreach grep !defined $handle_map{$_}, + qw(stdin stdout stderr logger status command); my $handles = GnuPG::Handles->new(%handle_map); return ($handles, \%handle_map); } -eval "require RT::Crypt::GnuPG_Vendor"; -if ($@ && $@ !~ qr{^Can't locate RT/Crypt/GnuPG_Vendor.pm}) { - die $@; -}; - -eval "require RT::Crypt::GnuPG_Local"; -if ($@ && $@ !~ qr{^Can't locate RT/Crypt/GnuPG_Local.pm}) { - die $@; -}; +RT::Base->_ImportOverlays(); # helper package to avoid using temp file package IO::Handle::CRLF;