pidgin-openpgp-0.1/XEP-0027.pl010064400000000000000000000451011136105710000144030ustar00rootroot# 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 or 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 . # # (C) 2008, Michael Braun 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 ", 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("",@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>/$1/; if ($message =~/\?NOGPG\?$/) { $message =~s/(.*)\?NOGPG\?$/[U]<\/span><\/i> $1/; } else { $message =~s/.*\?PGP\?/[E]<\/span><\/i> /; } $message = "".$message.""; @_[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",@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@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 enabled 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 disabled 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", "Cannot encrypt last message.", 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(",",@_)."\n"); # #} # #sub conv_created { # Purple::Debug::misc("openpgpplugin", "conv created:".join(",",@_)."\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"); }