[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