[Pidgin] #288: XEP-0027: OpenPGP in Jabber

Pidgin trac at pidgin.im
Sat Nov 15 09:29:43 EST 2008


#288: XEP-0027: OpenPGP in Jabber
-----------------------------+----------------------------------------------
 Reporter:  gagern           |        Owner:  nwalp
     Type:  enhancement      |       Status:  new  
Milestone:  Patches welcome  |    Component:  XMPP 
  Version:  2.0              |   Resolution:       
 Keywords:                   |  
-----------------------------+----------------------------------------------

Comment(by AZ):

 {{{
 # XEP-0027 Plugin for Pidgin (GTK).
 #
 # This plugin implements encryption and decryption of
 # jabber messages according to XEP-0027.
 # It does not sign nor verify <presence> or <status> messages,
 # as these only indicate that XEP-0027 is present at the remote party.
 #
 # Configuration:
 #  * configure gpg-agent manually
 #  * make sure gpg and gpg-agent are in %PATH%
 #  * JID => GPG key mapping is done search for jid in gpg,
 #    but may be overridden using config dialog.
 #
 # I don't take any liabilitity.
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    the Free Software Foundation, either version 3 of the License, or
 #    (at your option) any later version.
 #
 #    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.  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, see <http://www.gnu.org/licenses/>.
 #
 # (C) 2008, Michael Braun <michael-dev at fami-braun.de>

 use strict;
 use File::Temp qw /tempfile tmpnam/;
 use Purple;
 use File::Touch;
 use Config::INI::Simple;
 #use Pidgin;

 use constant TRUE => 1  ;
 use constant FALSE => 0 ;
 use constant CONFIGFILENAME => "pidgin-openpgp.ini";

 # *********** PLUGIN META DATA *************
 our %PLUGIN_INFO = (
     perl_api_version => 2,
     name => "OpenPGP Plugin",
     version => "0.1",
     summary => "Send and receive gpg encrypted messages.",
     description => "XEP-0027",
     author => "Michael Braun <michael-dev\@fami-braun.de>",
     url => "http://www.fami-braun.de",
     load => "plugin_load",
     unload => "plugin_unload",
     prefs_info => "prefs_info_cb",
 );

 my %CONNSTATE = ();
 my %GPGMAP = ();
 my $gpg = qx(gpg-agent --daemon)."gpg";

 sub plugin_init {
     return %PLUGIN_INFO;
 }

 # *********** DECODE received messages **********
 # TODO: fork into background, timeout, check return code of gpg, asking
 for key
 #       send error message on decryption failure
 sub decrypt {
     my $ciphertext = shift;
     my ($input, $fni) = tempfile();
     chmod 0600, $fni;

     print $input "-----BEGIN PGP MESSAGE-----\n\n$ciphertext\n-----END PGP
 MESSAGE-----\n";
     close $input;

     my $fno = tmpnam();

     my $ret = system("$gpg --batch --output $fno --use-agent -q -d $fni");

     if ($ret != 0) {
        return "*decryption failed*";
     }

     my $output;
     open $output, "<", $fno;
     my @plain = <$output>;
     close $output;

     unlink $fni;
     unlink $fno;

     return join("", at plain);
 }

 #******* verify received messages *******
 # TODO: implement

 # *********** INCOMING handler *************
 # TODO: indicate encryption state of message, replace body content,
 # OPTIONAL: detect signed presence and status tags and inform the user
 about
 #           the remote capabilites
 sub conv_receiving_jabber
 {
     my ($conn, $node, $data) = @_;
     my $encrypted_node =
 $node->get_child_with_namespace("x","jabber:x:encrypted");
     my $body = $node->get_child("body");
     if (not defined($encrypted_node)) {
        Purple::Debug::misc("         *    ", "no opengpg message");
        if (defined($body)) {
           my $newmsg ="?NOGPG?";
           $node->get_child("body")->insert_data($newmsg,length($newmsg));
        }
     } else {
        Purple::Debug::misc("opengpg received",
 $conn->get_display_name().", ".$node->get_attrib("id").", $data\n");
        my $crypted = $encrypted_node->get_data();
        my $plaintext = decrypt($crypted);
        #does not work: results in no message to be shown
        #$node->get_child("body")->free();
        #$node->new_child("body");
        my $newmsg = "?PGP?$plaintext";
        $node->get_child("body")->insert_data($newmsg,length($newmsg));
     }
     @_[2] = $node;
     Purple::Debug::misc("opengpg received", $node->to_str(0)."\n");
     #return $node;
 }

 sub conv_receiving_msg
 {
        my ($account, $from, $message, $conv, $flags, $data) = @_;
        my $xm = @_[2];

        Purple::Debug::misc("received", "$message\n");
        $message =~s/<body>(.*)<\/body>/$1/;

        if ($message =~/\?NOGPG\?$/) {
           $message =~s/(.*)\?NOGPG\?$/<i><span
 title="unencrypted">[U]<\/span><\/i> $1/;
        } else {
           $message =~s/.*\?PGP\?/<i><span
 title="encrypted">[E]<\/span><\/i> /;
        }

        $message = "<body>".$message."</body>";
        @_[2] = $message;
        Purple::Debug::misc("openpgpplugin", "replaced: $message\n");
 }

 # *********** ENCRYPT outgoing messages ************
 # TODO: fork into background, display error message
 sub encrypt {
     my $plaintext = shift;
     my $target = shift;

     if (exists($GPGMAP{$target})) {
       $target = $GPGMAP{$target};
     }

     my ($input, $fni) = tempfile();
     chmod 0600, $fni;

     print $input $plaintext;
     close $input;

     my $fno = tmpnam();

     my $ret = system("$gpg --batch --output $fno --use-agent -q --armor -r
 \"$target\" -e $fni");

     my $output;
     open $output, "<", $fno;
     my @plain = <$output>;
     chomp(@plain);
     close $output;

     unlink $fni;
     unlink $fno;

     if ($ret > 0) {
        Purple::Debug::misc("openpgp","encryption failed\n");
        return "";
     }

     # find first empty line
     for (; not ($plain[0] eq "");) {shift(@plain);};
     shift(@plain);
     pop(@plain);
     Purple::Debug::misc("openpgp","encryption successfull\n");

     return join("\n", at plain);
 }

 # *********** SIGN outgoing messages *********
 # TODO: implement
 #
 # ********** OUTGOING handler ************
 # encrypt outgoing message nodes and sign outgoing status and presence
 nodes
 # TODO: implement, configure key to use

 sub info_enable_gpg {
   my $target = shift;
   require Gtk2;
   my $frame = Gtk2::Window->new();
   my $dialog = Gtk2::MessageDialog->new ($frame,
                                       'destroy-with-parent',
                                       'info', # message type
                                       'ok', # which set of buttons?
                                       "Encryption for $target enabled.");
   $dialog->run;
   $dialog->destroy;
   $frame->destroy;
 }

 sub info_disable_gpg {
   my $target = shift;
   require Gtk2;
   my $frame = Gtk2::Window->new();
   my $dialog = Gtk2::MessageDialog->new ($frame,
                                       'destroy-with-parent',
                                       'info', # message type
                                       'ok', # which set of buttons?
                                       "Encryption for $target disabled.");
   $dialog->run;
   $dialog->destroy;
   $frame->destroy;
 }

 sub info_err_encrypt {
   my $target = shift;

   require Gtk2;
   my $frame = Gtk2::Window->new();
   my $dialog = Gtk2::MessageDialog->new ($frame,
                                       'destroy-with-parent',
                                       'error', # message type
                                       'cancel', # which set of buttons?
                                       "Could not encrypt message for
 $target.\nPlease check gpg settings and verify gpg-agent is running.");
   $dialog->run;
   $dialog->destroy;
   $frame->destroy;
 }

 sub conv_sending_msg
 {
     my ($conn, $node, $data) = @_;

     # get text node
     my $bnode = $node->get_child("body");
     if (not defined($bnode)) { return; }

     # fetch target / connid
     my $target = $node->get_attrib("to");
     $target =~s/\/.*//; # name at host/path => remove path
     my $connid = $target;
     if (not exists($CONNSTATE{$connid})) {$CONNSTATE{$connid} = 1; } #
 default off
     Purple::Debug::misc("openpgp","sending to $target\n");

     # fetch message
     my $msg = $bnode->get_data();

     # parse commands, decide on encryption
     my $do_encrypt = $CONNSTATE{$connid};
     Purple::Debug::misc("openpgp","sending message = $msg\n");
     if ($msg =~/^ENABLEPGP/) {
       Purple::Debug::misc("openpgp","enable pgp\n");
       $CONNSTATE{$connid} = 0;
       $msg = "The remote party <b>enabled</b> XEP-0027 (OpenPGP)
 encryption.";
       $do_encrypt = 0;
       info_enable_gpg($target);
     } elsif ($msg =~/^DISABLEPGP/) {
       Purple::Debug::misc("openpgp","disable pgp\n");
       $CONNSTATE{$connid} = 1;
       $msg = "The remote party <b>disabled</b> XEP-0027 (OpenPGP)
 encryption.";
       info_disable_gpg($target);
     }

     if ($do_encrypt == 1) { return; }

     # drop html node
     my $htmlbnode = $node->get_child("html");
     if (defined($htmlbnode)) { $htmlbnode->free(); }

     # encrypt data
     my $crypted = encrypt($msg, $target);

     if ($crypted eq "") {
       Purple::Debug::misc("openpgp","sending error message\n");
       info_err_encrypt($target);
       #$conn->get_im_data()->write("OpenPGP", "<b>Cannot encrypt last
 message.</b>", 0, 0);
       #$node->free(); -> crashes.
       # remove plain data
       $msg = "Failed to encrypt message.";
       $bnode->free();
       $node->new_child("body")->insert_data($msg, length($msg));
       @_[1] = $node;
       return;
     }

     # insert encrypted data
     my $x = $node->new_child("x");
     $x->set_attrib("xmlns","jabber:x:encrypted");
     $x->insert_data($crypted, length($crypted));

     # remove plain data
     $msg = "This is a protected copy.";
     $bnode->free();
     $node->new_child("body")->insert_data($msg, length($msg));

     # ensure new node is used!
     @_[1] = $node;
     Purple::Debug::misc("openpgp sending new", $node->to_str(0)."\n");
 }

 #****** modified conversation ******
 # here to come: integrate into conversation window
 #sub conv_switched {
 #   Purple::Debug::misc("openpgpplugin", "conv switched\n");
 #
 #}
 #
 #sub conv_deleted {
 #   Purple::Debug::misc("openpgpplugin", "conv
 deleted:".join(",", at _)."\n");
 #
 #}
 #
 #sub conv_created {
 #   Purple::Debug::misc("openpgpplugin", "conv
 created:".join(",", at _)."\n");
 #  my $conv = shift; # PurpleConversation
 #  init_dialog($conv);
 #}

 sub init_dialog {
 #  require Gtk2;
 #  require Pidgin::IMHtmlToolbar;
 #  Purple::Debug::misc("openpgpplugin", "conv init\n");
 #
 #  my $conv = shift;
 #  my $button = Gtk2::Button->new();
 #  $button->set_relief("GTK_RELIEF_NONE");
 #  bbox = gtkconv->toolbar;
 #
 #  gtk_box_pack_start(GTK_BOX(bbox), button, FALSE, FALSE, 0);
 #
 #    bwbox = gtk_hbox_new(FALSE, 0);
 #    gtk_container_add(GTK_CONTAINER(button), bwbox);
 #    icon = otr_icon(NULL, TRUST_NOT_PRIVATE, 1);
 #    gtk_box_pack_start(GTK_BOX(bwbox), icon, TRUE, FALSE, 0);
 #    label = gtk_label_new(NULL);
 #    gtk_box_pack_start(GTK_BOX(bwbox), label, FALSE, FALSE, 0);
 #
 #    if (prefs.show_otr_button) {
 #        gtk_widget_show_all(button);
 #    }
 }

 # ************ CONFIG handler **********
 # configure keys to use per contact and per account / global
 # TODO: implement, where to store this information
 my $LOCKED = 0;
 my %JIDINLINE = ();
 my %ITEMS=();

 sub info_err_savecfg {
   my $target = shift;

   require Gtk2;
   my $frame = Gtk2::Window->new();
   my $dialog = Gtk2::MessageDialog->new ($frame,
                                       'destroy-with-parent',
                                       'error', # message type
                                       'cancel', # which set of buttons?
                                       "Could not save config in
 $target.");
   $dialog->run;
   $dialog->destroy;
   $frame->destroy;
 }

 sub SaveCfg {
   my $cfgfile =
 Purple::Prefs::get_string("/plugins/core/openpgp/configfile");
   Purple::Debug::misc("openpgpplugin", "save:" .join(",",keys(%GPGMAP))."
 => ".join(",",values(%GPGMAP))." into $cfgfile\n");

   if (not -e $cfgfile) {
     touch($cfgfile);
   }

   if (not -w $cfgfile) {
     info_err_savecfg($cfgfile);
     return;
   }
   my $conf = new Config::INI::Simple;
   foreach my $key (keys(%GPGMAP)) {
     $conf->{default}->{$key} = $GPGMAP{$key};
   }
   $conf->write($cfgfile);
 }

 # file content:
 # JID=key
 sub LoadCfg {
   my $conf = new Config::INI::Simple;
   my $cfgfile =
 Purple::Prefs::get_string("/plugins/core/openpgp/configfile");
   if (not -r $cfgfile) {
    %GPGMAP = ();
    return;
   }

   $conf->read($cfgfile);
   use Data::Dumper;
   Purple::Debug::misc("openpgpplugin", Dumper($conf->{default})."\n");
   %GPGMAP = ();
   foreach my $key (keys(%{$conf->{default}})) {
     $GPGMAP{$key} = $conf->{default}->{$key};
   }
   Purple::Debug::misc("openpgpplugin", "load:" .join(",",keys(%GPGMAP))."
 => ".join(",",values(%GPGMAP))."\n");
 }

 sub delete_event {
   Purple::Debug::misc("openpgpplugin", "closing config window\n");
     # closing config window
   $LOCKED = 0;
 }

 sub on_ok {
   Purple::Debug::misc("openpgpplugin", "ok pressed\n");
   my $self = shift;
   my $frame = shift;
   $frame->destroy;
 }

 sub on_add {
   Purple::Debug::misc("openpgpplugin", "add pressed\n");
   my $self = shift;
   my $data = shift;
   my $ppref1 = $data->[0];
   my $ppref2 = $data->[1];
   my $xtable = $data->[2];
   my $jid = $ppref1->get_text();
   my $key = $ppref2->get_text();
   if (exists($GPGMAP{$jid})) {
     $GPGMAP{$jid} = $key;
     Purple::Debug::misc("openpgpplugin", "replacing $jid => $key\n");
     my $i = $JIDINLINE{$jid};
     $ITEMS{$i}->[1]->set_text($key);
     &SaveCfg;
   } else {
     my $i = keys(%GPGMAP) +2;
     Purple::Debug::misc("openpgpplugin", "adding $jid => $key with
 i=$i\n");
     $GPGMAP{$jid} = $key;
     $xtable->resize($i+1, 3);
     add_to_table($xtable, $jid, $i);
     &SaveCfg;
   }
   $ppref1->set_text("");
   $ppref2->set_text("");
 }

 sub on_del {
   Purple::Debug::misc("openpgpplugin", "del pressed\n");
   my $self = shift;
   my $data = shift;

   my $xtable = $data->[0];
   my $i = $data->[1];
   my $jid = $ITEMS{$i}->[3];

   # remove from GPGMAP
   Purple::Debug::misc("openpgpplugin", "deleting entry $i ($jid =>
 $GPGMAP{$jid})\n");
   delete($GPGMAP{$jid});

   # move all consecutive items up
   for (my $j = $i; $j < keys(%ITEMS)+1; $j++) {
     my $jid = $ITEMS{$j+1}->[3];
     $ITEMS{$j}->[3] = $jid; # move jid down
     $ITEMS{$j}->[0]->set_text($ITEMS{$j+1}->[0]->get_text()); # move jid
 label down
     $ITEMS{$j}->[1]->set_text($ITEMS{$j+1}->[1]->get_text()); # move key
 label down
     $JIDINLINE{$jid} = $j;
   }

   # remove last line
   my $j = keys(%ITEMS)+1;
   $ITEMS{$j}->[0]->destroy;
   $ITEMS{$j}->[1]->destroy;
   $ITEMS{$j}->[2]->destroy;
   delete($ITEMS{$j});
   $xtable->resize(keys(%ITEMS)+2,3);

   &SaveCfg;
 }

 sub add_to_table {
   my ($xtable, $jid, $i) = @_;

   Purple::Debug::misc("openpgpplugin", "show $jid\n");

   my $ppref1 = Gtk2::Label->new("$jid");
   $xtable->attach_defaults($ppref1, 0, 1, $i, $i+1);
   $ppref1->set_selectable(TRUE);
   $ppref1->show;

   my $value = $GPGMAP{$jid};
   my $ppref2 = Gtk2::Label->new("$value");
   $xtable->attach_defaults($ppref2, 1, 2, $i, $i+1);
   $ppref2->set_selectable(TRUE);
   $ppref2->show;

   my $button = Gtk2::Button->new("Del");
   $button->signal_connect(clicked => \&on_del, [$xtable, $i]);
   $xtable->attach_defaults($button, 2, 3, $i, $i+1);
   $button->show;

   $JIDINLINE{$jid} = $i;
   $ITEMS{$i} = [$ppref1, $ppref2, $button, $jid];
 }

 sub prefs_info_cb {
     Purple::Debug::misc("openpgpplugin", "cb\n");
     if ($LOCKED > 0) { return; }
     $LOCKED = 1;
   # *** JID => GPG-KeyID ***
     require Gtk2;
     my $frame = Gtk2::Window->new("toplevel");
     $frame->set_title("OpenPGP Plugin Konfiguration");
     $frame->signal_connect(delete_event => \&delete_event);

     my $box1 = Gtk2::VBox->new(FALSE, 0);
     $frame->add($box1);
     $box1->show;

     my $ppref = Gtk2::Label->new("Start gpg-agent first.");
     $box1->pack_start($ppref, TRUE, TRUE, 0);
     $ppref->show;

     my $ppref = Gtk2::Label->new("Use ENABLEPGP in conversation to enable
 encryption.");
     $box1->pack_start($ppref, TRUE, TRUE, 0);
     $ppref->show;

     my $ppref = Gtk2::Label->new("Use DISABLEPGP in conversation to
 disable encryption.");
     $box1->pack_start($ppref, TRUE, TRUE, 0);
     $ppref->show;

     my $ppref = Gtk2::Label->new("This plugin will DEFAULT to the jabber-
 id to lookup the remote gpg key.\nThe gpg binary is searched in the common
 (OS-dependent) path.");
     $box1->pack_start($ppref, TRUE, TRUE, 0);
     $ppref->show;

     my $separator = Gtk2::HSeparator->new;
     $box1->pack_start($separator, TRUE, TRUE, 0);
     $separator->show;
     # *** maps ***
     my $xtable = Gtk2::Table->new(keys(%GPGMAP)+1,3,TRUE);
     $box1->pack_start($xtable, TRUE, TRUE, 0);
     $xtable->show;
     my $i = 0;

     my $ppref1 = Gtk2::Label->new("JID");
     $xtable->attach_defaults($ppref1, 0, 1, $i, $i+1);
     $ppref1->set_selectable(FALSE);
     $ppref1->show;

     my $ppref2 = Gtk2::Label->new("Key-ID");
     $xtable->attach_defaults($ppref2, 1, 2, $i, $i+1);
     $ppref2->set_selectable(FALSE);
     $ppref2->show;

     $i = $i + 1;

     my $ppref1 = Gtk2::Entry->new;
     $xtable->attach_defaults($ppref1, 0, 1, $i, $i+1);
     $ppref1->show;

     my $ppref2 = Gtk2::Entry->new;
     $xtable->attach_defaults($ppref2, 1, 2, $i, $i+1);
     $ppref2->show;

     my $button = Gtk2::Button->new("Add");
     $button->signal_connect(clicked => \&on_add, [$ppref1, $ppref2,
 $xtable]);
     $xtable->attach_defaults($button, 2, 3, $i, $i+1);
     $button->show;

     $i = $i + 1;

     foreach my $jid (keys(%GPGMAP)) {
        add_to_table($xtable, $jid, $i);

        $i=$i+1;
     }

     my $separator = Gtk2::HSeparator->new;
     $box1->pack_start($separator, TRUE, TRUE, 0);
     $separator->show;

     # *** buttons ****
     my $button = Gtk2::Button->new("Ok");
     $button->signal_connect(clicked => \&on_ok, $frame);
     $box1->pack_start($button, TRUE, TRUE, 0);
     $button->show;

     $frame->show;

     return undef;
 }

 # ****** ONLOAD *******
 sub plugin_load {
     my $plugin = shift;
     Purple::Debug::misc("openpgpplugin", "plugin_load() - OpenPGP Plugin
 Loaded.\n");
     Purple::Prefs::add_none("/plugins/core/openpgp");
     Purple::Prefs::add_string("/plugins/core/openpgp/configfile",
 Purple::Util::user_dir()."/".CONFIGFILENAME);

     # A pointer to the handle to which the signal belongs needed by the
 callback function
     my $accounts_handle = Purple::Accounts::get_handle();
     my $jabber = Purple::Find::prpl("prpl-jabber");
     Purple::Signal::connect($jabber, "jabber-receiving-xmlnode", $plugin,
 \&conv_receiving_jabber, "receiving jabber node");
     Purple::Signal::connect($jabber, "jabber-sending-xmlnode", $plugin,
 \&conv_sending_msg, "sending jabber node");

     my $conv = Purple::Conversations::get_handle();
     Purple::Signal::connect($conv, "receiving-im-msg", $plugin,
 \&conv_receiving_msg, "receiving im message");
 #    Purple::Signal::connect($conv, "conversation-switched", $plugin,
 \&conv_switched, "conversation switched");
 #    Purple::Signal::connect($conv, "deleting-conversation", $plugin,
 \&conv_deleted, "conversation deleted");
 #    Purple::Signal::connect($conv, "conversation-created", $plugin,
 \&conv_created, "conversation created");

    &LoadCfg();
 }

 # ****** ON UNLOAD *******
 sub plugin_unload {
     my $plugin = shift;
     Purple::Debug::misc("openpgpplugin", "plugin_unload() - OpenPGP Plugin
 Unloaded.\n");
 }


 }}}

-- 
Ticket URL: <http://developer.pidgin.im/ticket/288#comment:17>
Pidgin <http://pidgin.im>
Pidgin


More information about the Tracker mailing list