# BEGIN BPS TAGGED BLOCK {{{
-#
+#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
-# <jesse@bestpractical.com>
-#
+#
+# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
# (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
# 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;
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
=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.
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()),
# ...
# );
@_
);
- my $gnupg = new GnuPG::Interface;
+ my $gnupg = GnuPG::Interface->new();
my %opt = RT->Config->Get('GnuPGOptions');
# handling passphrase in GnuPGOptions
);
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
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;
);
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
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(
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));
);
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
# 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 ) {
+ pipe( my ($read_decoded, $write_decoded) );
+ my $decoder = MIME::Decoder->new( $entity->head->mime_encoding );
+ if ($decoder) {
+ eval { $decoder->decode($io, $write_decoded) };
+ $io = $read_decoded;
+ }
+ }
+
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;
# 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 ) {
=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;
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(
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(
@_
);
- my $gnupg = new GnuPG::Interface;
+ my $gnupg = GnuPG::Interface->new();
my %opt = RT->Config->Get('GnuPGOptions');
# handling passphrase in GnupGOptions
}
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( [] );
@_
);
- my $gnupg = new GnuPG::Interface;
+ my $gnupg = GnuPG::Interface->new();
my %opt = RT->Config->Get('GnuPGOptions');
# handling passphrase in GnuPGOptions
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;
}
$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;
}
@_
);
- my $gnupg = new GnuPG::Interface;
+ my $gnupg = GnuPG::Interface->new();
my %opt = RT->Config->Get('GnuPGOptions');
# handling passphrase in GnuPGOptions
);
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;
@_
);
- my $gnupg = new GnuPG::Interface;
+ my $gnupg = GnuPG::Interface->new();
my %opt = RT->Config->Get('GnuPGOptions');
# handling passphrase in GnupGOptions
=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
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 {
# 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;
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
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;
};
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 );
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 ),
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'} ) {
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 ),
eval {
local $SIG{'CHLD'} = 'DEFAULT';
- local @ENV{'LANG', 'LC_ALL'} = ('C', 'C');
my $pid = safe_run_child { $gnupg->wrap_call(
handles => $handles,
commands => ['--import'],
sub Probe {
- my $gnupg = new GnuPG::Interface;
+ my $gnupg = GnuPG::Interface->new();
my %opt = RT->Config->Get('GnuPGOptions');
$gnupg->options->hash_init(
_PrepareGnuPGOptions( %opt ),
# 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;
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;