Merge branch 'master' of https://github.com/jgoodman/Freeside
[freeside.git] / rt / lib / RT / Crypt / GnuPG.pm
index 29dd2a9..d0587d4 100644 (file)
@@ -2,7 +2,7 @@
 #
 # COPYRIGHT:
 #
-# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
@@ -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</opt/rt3/var/data/gpg>.
+The GnuPG home directory, by default it is set to F</opt/rt4/var/data/gpg>.
 
 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 ) {
@@ -1060,9 +1091,13 @@ sub VerifyDecrypt {
         }
         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 ) {
@@ -1079,9 +1114,13 @@ sub VerifyDecrypt {
         }
         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;
@@ -1092,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(
@@ -1146,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(
@@ -1199,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
@@ -1261,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( [] );
@@ -1277,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
@@ -1373,7 +1412,7 @@ sub DecryptInline {
     }
 
     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;
 }
@@ -1436,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
@@ -1469,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;
@@ -1487,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
@@ -1597,8 +1643,7 @@ User friendly message.
 
 =back
 
-This parser is based on information from GnuPG distribution, see also
-F<docs/design_docs/gnupg_details_on_output_formats> in the RT distribution.
+This parser is based on information from GnuPG distribution.
 
 =cut
 
@@ -1673,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 {
@@ -2000,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;
@@ -2076,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
@@ -2096,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;
     };
@@ -2112,7 +2160,10 @@ sub GetKeysInfo {
     }
     $RT::Logger->debug( $res{'status'} ) if $res{'status'};
     $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
-    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+    if ( $res{'logger'} && $? ) {
+        $RT::Logger->error( $res{'logger'} );
+        $RT::Logger->error( 'The above error may result from an unconfigured RT/GPG installation. See perldoc etc/RT_Config.pm for information about configuring or disabling GPG support for RT' );
+    }
     if ( $@ || $? ) {
         $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
         return %res;
@@ -2262,7 +2313,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 );
@@ -2275,7 +2326,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 ),
@@ -2287,11 +2338,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'} ) {
@@ -2323,7 +2373,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 ),
@@ -2335,7 +2385,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'],
@@ -2406,7 +2455,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 ),
@@ -2463,15 +2512,7 @@ sub _make_gpg_handles {
     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;