[Rt-commit] r10508 - in rt/branches/3.7-EXPERIMENTAL: . etc html/Ticket html/Ticket/Elements lib/RT

ruz at bestpractical.com ruz at bestpractical.com
Sat Jan 26 20:01:30 EST 2008


Author: ruz
Date: Sat Jan 26 20:01:30 2008
New Revision: 10508

Added:
   rt/branches/3.7-EXPERIMENTAL/html/Ticket/GnuPG.html
Modified:
   rt/branches/3.7-EXPERIMENTAL/   (props changed)
   rt/branches/3.7-EXPERIMENTAL/etc/RT_Config.pm.in
   rt/branches/3.7-EXPERIMENTAL/html/Ticket/Elements/ShowTransaction
   rt/branches/3.7-EXPERIMENTAL/lib/RT/Attachment_Overlay.pm
   rt/branches/3.7-EXPERIMENTAL/lib/RT/Crypt/GnuPG.pm

Log:
 r10492 at cubic-pc (orig r10458):  ruz | 2008-01-24 02:13:46 +0300
 * add configurable option that allow people to encrypt
   attachments right in the DB
 


Modified: rt/branches/3.7-EXPERIMENTAL/etc/RT_Config.pm.in
==============================================================================
--- rt/branches/3.7-EXPERIMENTAL/etc/RT_Config.pm.in	(original)
+++ rt/branches/3.7-EXPERIMENTAL/etc/RT_Config.pm.in	Sat Jan 26 20:01:30 2008
@@ -313,6 +313,10 @@
 # Set OutgoingMessagesFormat to 'inline' to use inline encryption and
 # signatures instead of 'RFC' (GPG/MIME: RFC3156 and RFC1847) format.
     OutgoingMessagesFormat => 'RFC', # Inline
+
+# If you want to allow people to encrypt attachments inside the DB then
+# set below option to true value
+    AllowEncryptDataInDB   => 0,
 );
 
 # Options of GnuPG program

Modified: rt/branches/3.7-EXPERIMENTAL/html/Ticket/Elements/ShowTransaction
==============================================================================
--- rt/branches/3.7-EXPERIMENTAL/html/Ticket/Elements/ShowTransaction	(original)
+++ rt/branches/3.7-EXPERIMENTAL/html/Ticket/Elements/ShowTransaction	Sat Jan 26 20:01:30 2008
@@ -89,6 +89,7 @@
 $AttachPath => RT->Config->Get('WebPath')."/Ticket/Attachment"
 $UpdatePath => RT->Config->Get('WebPath')."/Ticket/Update.html"
 $ForwardPath => RT->Config->Get('WebPath')."/Ticket/Forward.html"
+$EncryptionPath => RT->Config->Get('WebPath')."/Ticket/GnuPG.html"
 $EmailRecordPath => RT->Config->Get('WebPath')."/Ticket/ShowEmailRecord.html"
 $Attachments => undef
 $AttachmentContent => undef
@@ -182,8 +183,9 @@
     }
 
     if ( $Attachments->[0] && $ShowTitleBarCommands ) {
-        my $can_modify = $Transaction->TicketObj->CurrentUserHasRight('ModifyTicket');
-        if ( $can_modify || $Transaction->TicketObj->CurrentUserHasRight('ReplyToTicket') ) {
+        my $ticket = $Transaction->TicketObj;
+        my $can_modify = $ticket->CurrentUserHasRight('ModifyTicket');
+        if ( $can_modify || $ticket->CurrentUserHasRight('ReplyToTicket') ) {
             $titlebar_commands .=
                 "[<a href=\"".$UpdatePath
               . "?id=" . $Transaction->Ticket
@@ -192,7 +194,7 @@
               . loc('Reply')
               . "</a>]&nbsp;";
         }
-        if ( $can_modify || $Transaction->TicketObj->CurrentUserHasRight('CommentOnTicket') ) {
+        if ( $can_modify || $ticket->CurrentUserHasRight('CommentOnTicket') ) {
             $titlebar_commands .=
                 "[<a href=\"".$UpdatePath."?id="
               . $Transaction->Ticket
@@ -201,13 +203,23 @@
               . "&Action=Comment\">"
               . loc('Comment') . "</a>]";
         }
-        if ( $Transaction->TicketObj->CurrentUserHasRight('ForwardMessage') ) {
+        if ( $ticket->CurrentUserHasRight('ForwardMessage') ) {
             $titlebar_commands .=
                 "[<a href=\"". $ForwardPath
               . "?id=". $Transaction->Ticket
               . "&QuoteTransaction=". $Transaction->Id
               . "\">". loc('Forward') . "</a>]";
         }
+        if ( $can_modify
+            && RT->Config->Get('GnuPG')->{'Enable'}
+            && RT->Config->Get('GnuPG')->{'AllowEncryptDataInDB'}
+            && $ticket->CurrentUserHasRight('ForwardMessage')
+        ) {
+            $titlebar_commands .=
+                "[<a href=\"". $EncryptionPath
+              . "?id=". $Transaction->Id
+              . "\">". loc('Ecnrypt/Decrypt') . "</a>]";
+        }
     }
 }
 </%INIT>

Added: rt/branches/3.7-EXPERIMENTAL/html/Ticket/GnuPG.html
==============================================================================
--- (empty file)
+++ rt/branches/3.7-EXPERIMENTAL/html/Ticket/GnuPG.html	Sat Jan 26 20:01:30 2008
@@ -0,0 +1,57 @@
+<& /Elements/Header, Title => $title &>
+<& /Ticket/Elements/Tabs,
+    Ticket => $txn->TicketObj,
+    current_tab => 'Ticket/Encrypt.html?id='. $id,
+    Title => $title,
+&>
+
+% $m->callback( CallbackName => 'BeforeActionList', %ARGS, Actions => \@results, ARGSRef => \%ARGS );
+<& /Elements/ListActions, actions => \@results &>
+<form method="post">
+<a href="<% RT->Config->Get('WebURL') %>/Ticket/Display.html?id=<% $txn->Ticket %>#txn-<% $id %>">
+<% loc('Return back to the ticket') %>
+</a>
+<& /Elements/Submit,
+    Label => ($encrypted? loc('Decrypt'): loc('Encrypt')),
+    Name  => ($encrypted? 'Decrypt': 'Encrypt'),
+&>
+</form>
+<%ARGS>
+$id => undef
+$Encrypt => 0
+$Decrypt => 0
+</%ARGS>
+<%INIT>
+my $txn = RT::Transaction->new( $session{'CurrentUser'} );
+$txn->Load( $id );
+unless ( $txn->id ) {
+    Abort(loc("Couldn't load transaction #[_1]", $id));
+}
+$id = $txn->id;
+
+my @results;
+
+my $encrypted = 0;
+
+my $attachments = $txn->Attachments;
+while ( my $attachment = $attachments->Next ) {
+    next unless $attachment->ContentType =~ m{^x-application-rt/gpg-encrypted\b};
+    $encrypted = 1;
+    last;
+}
+$attachments->GotoFirstItem;
+
+if ( $Encrypt || $Decrypt ) {
+    my $done = 1;
+    while ( my $attachment = $attachments->Next ) {
+        my ($status, $msg) = $Decrypt? $attachment->Decrypt : $attachment->Encrypt;
+        push @results, $msg;
+        unless ( $status ) {
+            $done = 0; last;
+        }
+    }
+    $encrypted = !$encrypted if $done;
+}
+
+my $title = loc("Encrypt/Decrypt transaction #[_1] of ticket #[_2]", $id, $txn->Ticket);
+</%INIT>

Modified: rt/branches/3.7-EXPERIMENTAL/lib/RT/Attachment_Overlay.pm
==============================================================================
--- rt/branches/3.7-EXPERIMENTAL/lib/RT/Attachment_Overlay.pm	(original)
+++ rt/branches/3.7-EXPERIMENTAL/lib/RT/Attachment_Overlay.pm	Sat Jan 26 20:01:30 2008
@@ -607,6 +607,103 @@
 }
 
 
+sub Encrypt {
+    my $self = shift;
+
+    my $txn = $self->TransactionObj;
+    return (0, $self->loc('Permission Denied')) unless $txn->CurrentUserCanSee;
+    return (0, $self->loc('Permission Denied'))
+        unless $txn->TicketObj->CurrentUserHasRight('ModifyTicket');
+    return (0, $self->loc('GnuPG integration is disabled'))
+        unless RT->Config->Get('GnuPG')->{'Enable'};
+    return (0, $self->loc('Attachments encryption is disabled'))
+        unless RT->Config->Get('GnuPG')->{'AllowEncryptDataInDB'};
+
+    require RT::Crypt::GnuPG;
+
+    my $type = $self->ContentType;
+    if ( $type =~ /^x-application-rt\/gpg-encrypted/i ) {
+        return (1, $self->loc('Already encrypted'));
+    } elsif ( $type =~ /^multipart\//i ) {
+        return (1, $self->loc('No need to encrypt'));
+    } else {
+        $type = qq{x-application-rt\/gpg-encrypted; original-type="$type"};
+    }
+
+    my $queue = $txn->TicketObj->QueueObj;
+    my $encrypt_for;
+    foreach my $address ( grep $_,
+        $queue->CorrespondAddress,
+        $queue->CommentAddress,
+        RT->Config->Get('CorrespondAddress'),
+        RT->Config->Get('CommentAddress'),
+    ) {
+        my %res = RT::Crypt::GnuPG::GetKeysInfo( $address, 'private' );
+        next if $res{'exit_code'} || !$res{'info'};
+        %res = RT::Crypt::GnuPG::GetKeysForEncryption( $address );
+        next if $res{'exit_code'} || !$res{'info'};
+        $encrypt_for = $address;
+    }
+    unless ( $encrypt_for ) {
+        return (0, $self->loc('No key suitable for encryption'));
+    }
+
+    $self->__Set( Field => 'ContentType', Value => $type );
+    $self->SetHeader( 'Content-Type' => $type );
+
+    my $content = $self->Content;
+    my %res = RT::Crypt::GnuPG::SignEncryptContent(
+        Content => \$content,
+        Sign => 0,
+        Encrypt => 1,
+        Recipients => [ $encrypt_for ],
+    );
+    if ( $res{'exit_code'} ) {
+        return (0, $self->loc('GnuPG error. Contact with administrator'));
+    }
+
+    my ($status, $msg) = $self->__Set( Field => 'Content', Value => $content );
+    unless ( $status ) {
+        return ($status, $self->loc("Couldn't replace content with encrypted data: [_1]", $msg));
+    }
+    return (1, $self->loc('Successfuly encrypted data'));
+}
+
+sub Decrypt {
+    my $self = shift;
+
+    my $txn = $self->TransactionObj;
+    return (0, $self->loc('Permission Denied')) unless $txn->CurrentUserCanSee;
+    return (0, $self->loc('Permission Denied'))
+        unless $txn->TicketObj->CurrentUserHasRight('ModifyTicket');
+    return (0, $self->loc('GnuPG integration is disabled'))
+        unless RT->Config->Get('GnuPG')->{'Enable'};
+
+    require RT::Crypt::GnuPG;
+
+    my $type = $self->ContentType;
+    if ( $type =~ /^x-application-rt\/gpg-encrypted/i ) {
+        ($type) = ($type =~ /original-type="(.*)"/i);
+        $type ||= 'application/octeat-stream';
+    } else {
+        return (1, $self->loc('Is not encrypted'));
+    }
+    $self->__Set( Field => 'ContentType', Value => $type );
+    $self->SetHeader( 'Content-Type' => $type );
+
+    my $content = $self->Content;
+    my %res = RT::Crypt::GnuPG::DecryptContent( Content => \$content, );
+    if ( $res{'exit_code'} ) {
+        return (0, $self->loc('GnuPG error. Contact with administrator'));
+    }
+
+    my ($status, $msg) = $self->__Set( Field => 'Content', Value => $content );
+    unless ( $status ) {
+        return ($status, $self->loc("Couldn't replace content with decrypted data: [_1]", $msg));
+    }
+    return (1, $self->loc('Successfuly decrypted data'));
+}
+
 =head2 _Value
 
 Takes the name of a table column.

Modified: rt/branches/3.7-EXPERIMENTAL/lib/RT/Crypt/GnuPG.pm
==============================================================================
--- rt/branches/3.7-EXPERIMENTAL/lib/RT/Crypt/GnuPG.pm	(original)
+++ rt/branches/3.7-EXPERIMENTAL/lib/RT/Crypt/GnuPG.pm	Sat Jan 26 20:01:30 2008
@@ -135,6 +135,13 @@
 
 =back
 
+=head3 Encrypting data in the database
+
+You can allow users to encrypt data in the database using
+option C<AllowEncryptDataInDB>. By default it's disabled.
+Users must have rights to see and modify tickets to use
+this feature.
+
 =head2 %GnuPGOptions
 
 Use this hash to set options of the 'gnupg' program. You can define almost any
@@ -800,6 +807,103 @@
     return %res;
 }
 
+sub SignEncryptContent {
+    my %args = (
+        Content => undef,
+
+        Sign => 1,
+        Signer => undef,
+        Passphrase => undef,
+
+        Encrypt => 1,
+        Recipients => undef,
+
+        @_
+    );
+    return unless $args{'Sign'} || $args{'Encrypt'};
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+    $opt{'digest-algo'} ||= 'SHA1';
+    $opt{'default_key'} = $args{'Signer'}
+        if $args{'Sign'} && $args{'Signer'};
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        armor => 1,
+        meta_interactive => 0,
+    );
+
+    # handling passphrase in GnupGOptions
+    $args{'Passphrase'} = delete $opt{'passphrase'}
+        if !defined($args{'Passphrase'});
+
+    if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
+        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
+    }
+
+    if ( $args{'Encrypt'} ) {
+        $gnupg->options->push_recipients( $_ ) foreach 
+            map UseKeyForEncryption($_) || $_,
+            @{ $args{'Recipients'} || [] };
+    }
+
+    my %res;
+
+    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile();
+    binmode $tmp_fh, ':raw';
+
+    my %handle;
+    my $handles = GnuPG::Handles->new(
+        stdin  => ($handle{'input'}  = new IO::Handle),
+        stdout => $tmp_fh,
+        stderr => ($handle{'error'}  = new IO::Handle),
+        logger => ($handle{'logger'} = new IO::Handle),
+        status => ($handle{'status'} = new IO::Handle),
+    );
+    $handles->options( 'stdout'  )->{'direct'} = 1;
+    $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};
+
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        my $method = $args{'Sign'} && $args{'Encrypt'}
+            ? 'sign_and_encrypt'
+            : ($args{'Sign'}? 'clearsign': 'encrypt');
+        my $pid = _safe_run_child { $gnupg->$method( handles => $handles ) };
+        $handle{'input'}->print( ${ $args{'Content'} } );
+        close $handle{'input'};
+        waitpid $pid, 0;
+    };
+    $res{'exit_code'} = $?;
+    my $err = $@;
+
+    foreach ( qw(error logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'error'} ) if $res{'error'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+    if ( $err || $res{'exit_code'} ) {
+        $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
+        return %res;
+    }
+
+    ${ $args{'Content'} } = '';
+    seek $tmp_fh, 0, 0;
+    while (1) {
+        my $status = read $tmp_fh, my $buf, 4*1024;
+        unless ( defined $status ) {
+            $RT::Logger->crit( "couldn't read message: $!" );
+        } elsif ( !$status ) {
+            last;
+        }
+        ${ $args{'Content'} } .= $buf;
+    }
+
+    return %res;
+}
+
 sub FindProtectedParts {
     my %args = ( Entity => undef, CheckBody => 1, @_ );
     my $entity = $args{'Entity'};
@@ -1245,6 +1349,86 @@
     return %res;
 }
 
+sub DecryptContent {
+    my %args = (
+        Content => undef,
+        Passphrase => undef,
+        @_
+    );
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+    $opt{'digest-algo'} ||= 'SHA1';
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        meta_interactive => 0,
+    );
+
+    # handling passphrase in GnupGOptions
+    $args{'Passphrase'} = delete $opt{'passphrase'}
+        if !defined($args{'Passphrase'});
+
+    $args{'Passphrase'} = GetPassphrase()
+        unless defined $args{'Passphrase'};
+
+    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile();
+    binmode $tmp_fh, ':raw';
+
+    my %handle;
+    my $handles = GnuPG::Handles->new(
+        stdin  => ($handle{'input'}  = new IO::Handle),
+        stdout => $tmp_fh,
+        stderr => ($handle{'error'}  = new IO::Handle),
+        logger => ($handle{'logger'} = new IO::Handle),
+        status => ($handle{'status'} = new IO::Handle),
+    );
+    $handles->options( 'stdout' )->{'direct'} = 1;
+
+    my %res;
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        $gnupg->passphrase( $args{'Passphrase'} );
+        my $pid = _safe_run_child { $gnupg->decrypt( handles => $handles ) };
+        print { $handle{'input'} } ${ $args{'Content'} };
+        close $handle{'input'};
+
+        waitpid $pid, 0;
+    };
+    $res{'exit_code'} = $?;
+    foreach ( qw(error logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'error'} ) if $res{'error'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+
+    # if the decryption is fine but the signature is bad, then without this
+    # status check we lose the decrypted text
+    # XXX: add argument to the function to control this check
+    if ( $res{'status'} !~ /DECRYPTION_OKAY/ ) {
+        if ( $@ || $? ) {
+            $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
+            return %res;
+        }
+    }
+
+    ${ $args{'Content'} } = '';
+    seek $tmp_fh, 0, 0;
+    while (1) {
+        my $status = read $tmp_fh, my $buf, 4*1024;
+        unless ( defined $status ) {
+            $RT::Logger->crit( "couldn't read message: $!" );
+        } elsif ( !$status ) {
+            last;
+        }
+        ${ $args{'Content'} } .= $buf;
+    }
+
+    return %res;
+}
+
 =head2 GetPassphrase [ Address => undef ]
 
 Returns passphrase, called whenever it's required with Address as a named argument.


More information about the Rt-commit mailing list