2013-04-13 20:33:06 +00:00
"""
This plugin implements the ` XEP - 0027 ` _ “ Current Jabber OpenPGP Usage ” .
This is a plugin used to encrypt one - to - one conversation using the PGP
encryption method . You can use it if you want really good privacy . Without this
encryption , your messages are encrypted * * at least * * from your client ( poezio ) to
your server . The message is decrypted by your server and you cannot control the
encryption method of your messages from your server to your contact ’ s server
( unless you are your own server ’ s administrator ) , nor from your contact ’ s
server to your contact ’ s client .
This plugin does end - to - end encryption . This means that * * only * * your contact can
decrypt your messages , and it is fully encrypted during * * all * * its travel
through the internet .
Note that if you are having an encrypted conversation with a contact , you can
* * not * * send XHTML - IM messages to him . They will be removed and be replaced by
plain text messages .
Installation and configuration
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
You should autoload this plugin , as it will send your signed presence directly
on login , making it easier for your contact ’ s clients to know that you are
supporting GPG encryption . To do that , use the : term : ` plugins_autoload ` configuration
option .
You need to create a plugin configuration file . Create a file named : file : ` gpg . cfg `
into your plugins configuration directory ( : file : ` ~ / . config / poezio / plugins ` by
default ) , and fill it like this :
. . code - block : : ini
[ gpg ]
keyid = 091 F9C78
passphrase = your OPTIONAL passphrase
[ keys ]
example @jabber.org = E3CFCDE2
juliet @xmpp.org = EF27ABCD
The ` ` gpg ` ` section is about your key . You need to specify the keyid , for the
key you want to use . You can as well provide a passphrase . If you don ’ t , you
should use a gpg agent or something like that that will ask your passphrase
whenever you need it .
The ` ` keys ` ` section contains your contact ’ s id keys . For each contact you want
to have encrypted conversations with , add her / his JID associated with the keyid
of his / her key .
And that ’ s it , now you need to talk directly to the * * full * * jid of your
contacts . Poezio doesn ’ t let you encrypt messages whom recipients is a bare
JID .
Additionnal information on GnuPG
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Create a key
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
To create a personal key , use
. . code - block : : bash
gpg - - gen - key
and follow the instructions .
Keyid
~ ~ ~ ~ ~
The keyid ( required in the gpg . cfg configuration file ) is a 8 character - long
key . You can get the ones you created or imported by using the command
. . code - block : : bash
gpg - - list - keys
You will get something like
. . code - block : : none
pub 4096 R / 01234567 2011 - 11 - 11
uid Your Name Here ( comment ) < email @example.org >
sub 4096 R / AAFFBBCC 2011 - 11 - 11
pub 2048 R / 12345678 2011 - 11 - 12 [ expire : 2011 - 11 - 22 ]
uid A contact ’ s name ( comment ) < fake @fake.fr >
sub 2048 R / FFBBAACC 2011 - 11 - 12 [ expire : 2011 - 11 - 22 ]
In this example , the keyids are ` ` 01234567 ` ` and ` ` 12345678 ` ` .
Share your key
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
Use :
. . code - block : : bash
gpg - - send - keys - - keyserver pgp . mit . edu < keyid >
to upload you public key on a public server .
. . _XEP - 0027 : http : / / xmpp . org / extensions / xep - 0027. html
"""
2011-11-11 22:44:26 +00:00
from gpg import gnupg
2011-11-13 17:16:38 +00:00
from sleekxmpp . xmlstream . stanzabase import JID
2011-11-12 01:48:13 +00:00
from xml . etree import cElementTree as ET
import xml . sax . saxutils
2011-11-11 22:44:26 +00:00
2011-11-12 01:48:13 +00:00
import logging
log = logging . getLogger ( __name__ )
2011-11-12 04:19:06 +00:00
from plugin import BasePlugin
2011-11-12 04:48:29 +00:00
from tabs import ConversationTab
2011-11-12 04:19:06 +00:00
2011-11-12 01:48:13 +00:00
NS_SIGNED = " jabber:x:signed "
NS_ENCRYPTED = " jabber:x:encrypted "
2011-11-12 04:19:06 +00:00
SIGNED_ATTACHED_MESSAGE = """ -----BEGIN PGP SIGNED MESSAGE-----
2011-11-12 21:24:05 +00:00
Hash : % ( hash ) s
2011-11-12 04:19:06 +00:00
% ( clear ) s
- - - - - BEGIN PGP SIGNATURE - - - - -
Version : GnuPG
% ( data ) s
- - - - - END PGP SIGNATURE - - - - -
"""
ENCRYPTED_MESSAGE = """ -----BEGIN PGP MESSAGE-----
Version : GnuPG
% ( data ) s
- - - - - END PGP MESSAGE - - - - - """
2011-11-11 22:44:26 +00:00
class Plugin ( BasePlugin ) :
def init ( self ) :
2011-11-12 01:48:13 +00:00
self . contacts = { }
2011-11-12 04:59:46 +00:00
# a dict of {full-JID: 'signed'/'valid'/'invalid'/'disabled'}
2011-11-12 01:48:13 +00:00
# Whenever we receive a signed presence from a JID, we add it to this
# dict, this way we know if we can encrypt the messages we will send to
# this JID.
# If that resource sends a non-signed presence, then we remove it
# from that dict and stop encrypting our messages.
2011-11-12 04:59:46 +00:00
# 'disabled' means that the user do NOT want to encrypt its messages
# even if the key is valid.
2011-11-12 01:48:13 +00:00
self . gpg = gnupg . GPG ( )
self . keyid = self . config . get ( ' keyid ' , ' ' ) or None
self . passphrase = self . config . get ( ' passphrase ' , ' ' ) or None
if not self . keyid :
self . core . information ( ' No GPG keyid provided in the configuration ' , ' Warning ' )
self . add_event_handler ( ' send_normal_presence ' , self . sign_presence )
self . add_event_handler ( ' normal_presence ' , self . on_normal_presence )
2011-11-12 02:44:12 +00:00
self . add_event_handler ( ' conversation_say_after ' , self . on_conversation_say )
self . add_event_handler ( ' conversation_msg ' , self . on_conversation_msg )
2011-11-12 01:48:13 +00:00
2013-03-01 18:25:31 +00:00
self . add_tab_command ( ConversationTab , ' gpg ' , self . command_gpg ,
usage = ' <force|disable|setkey> [jid] [keyid] ' ,
help = ' Force or disable gpg encryption with the fulljid of the current conversation. The setkey argument lets you associate a keyid with the given bare JID. ' ,
short = ' Manage the GPG status ' ,
completion = self . gpg_completion )
2011-11-12 04:48:29 +00:00
ConversationTab . add_information_element ( ' gpg ' , self . display_encryption_status )
2011-11-12 01:48:13 +00:00
def cleanup ( self ) :
self . send_unsigned_presence ( )
2011-11-12 04:48:29 +00:00
ConversationTab . remove_information_element ( ' gpg ' )
2011-11-12 04:59:46 +00:00
self . del_tab_command ( ConversationTab , ' gpg ' )
2011-11-12 01:48:13 +00:00
def sign_presence ( self , presence ) :
"""
Sign every normal presence we send
"""
2011-11-12 02:44:12 +00:00
signed_element = ET . Element ( ' { %s }x ' % ( NS_SIGNED , ) )
2011-11-12 04:19:06 +00:00
t = self . gpg . sign ( presence [ ' status ' ] , keyid = self . keyid , passphrase = self . passphrase , detach = True )
2011-11-12 01:48:13 +00:00
if not t :
self . core . information ( ' Could not sign presence. Disabling GPG module ' , ' Info ' )
self . core . plugin_manager . unload ( ' gpg ' )
return
2011-11-12 04:19:06 +00:00
text = xml . sax . saxutils . escape ( str ( t ) )
signed_element . text = self . remove_gpg_headers ( text )
2011-11-12 01:48:13 +00:00
presence . append ( signed_element )
def send_unsigned_presence ( self ) :
"""
Send our current presence , to everyone , but unsigned , to indicate
2011-11-12 02:44:12 +00:00
that we cannot / do not want to encrypt / decrypt messages .
2011-11-12 01:48:13 +00:00
"""
current_presence = self . core . get_status ( )
2014-02-03 19:57:40 +00:00
self . core . command_status ( ' %s %s ' % ( current_presence . show or ' available ' , current_presence . message or ' ' , ) )
2011-11-11 22:44:26 +00:00
2011-11-12 01:48:13 +00:00
def on_normal_presence ( self , presence , resource ) :
"""
Check if it ’ s signed , if it is and we can verify the signature ,
add ' valid ' or ' invalid ' into the dict . If it cannot be verified , just add
' signed ' . Otherwise , do nothing .
"""
signed = presence . find ( ' { %s }x ' % ( NS_SIGNED , ) )
bare = presence [ ' from ' ] . bare
full = presence [ ' from ' ] . full
if signed is None :
if bare in self . contacts . keys ( ) :
del self . contacts [ bare ]
return
if self . config . has_section ( ' keys ' ) and bare in self . config . options ( ' keys ' ) :
2011-11-12 21:24:05 +00:00
self . contacts [ full ] = ' invalid '
for hash_ in ( ' SHA1 ' , ' SHA256 ' ) :
to_verify = SIGNED_ATTACHED_MESSAGE % { ' clear ' : presence [ ' status ' ] ,
' data ' : signed . text ,
' hash ' : hash_ }
verify = self . gpg . verify ( to_verify )
if verify :
self . contacts [ full ] = ' valid '
break
2011-11-12 01:48:13 +00:00
else :
self . contacts [ full ] = ' signed '
2011-11-12 02:44:12 +00:00
def on_conversation_say ( self , message , tab ) :
"""
Check if the contact has a signed AND veryfied signature .
If yes , encrypt the message with her key .
"""
to = message [ ' to ' ]
if not message [ ' body ' ] :
# there’ s nothing to encrypt if this is a chatstate, for example
return
signed = to . full in self . contacts . keys ( )
if signed :
2011-11-13 17:16:38 +00:00
veryfied = self . contacts [ to . full ] in ( ' valid ' , ' forced ' )
2011-11-12 02:44:12 +00:00
else :
veryfied = False
if veryfied :
# remove the xhtm_im body if present, because that
# cannot be encrypted.
2013-08-06 17:09:13 +00:00
del message [ ' html ' ]
2011-11-12 02:44:12 +00:00
encrypted_element = ET . Element ( ' { %s }x ' % ( NS_ENCRYPTED , ) )
2012-11-05 14:54:33 +00:00
text = self . gpg . encrypt ( message [ ' body ' ] , self . config . get ( to . bare , ' ' , section = ' keys ' ) , always_trust = True )
if not text :
self . core . information ( ' Could not encrypt message to %s ' % ( to . full ) , )
# If we could not encrypt the message, don't send anything
message [ ' body ' ] = ' '
return
encrypted_element . text = self . remove_gpg_headers ( xml . sax . saxutils . escape ( str ( text ) ) )
2011-11-12 02:44:12 +00:00
message . append ( encrypted_element )
2012-11-05 14:54:33 +00:00
message [ ' body ' ] = ' This message has been encrypted using the GPG key with id: %s ' % self . keyid
2011-11-12 02:44:12 +00:00
def on_conversation_msg ( self , message , tab ) :
"""
Check if the message is encrypted , and decrypt it if we can .
"""
encrypted = message . find ( ' { %s }x ' % ( NS_ENCRYPTED , ) )
fro = message [ ' from ' ]
if encrypted is not None :
if self . config . has_section ( ' keys ' ) and fro . bare in self . config . options ( ' keys ' ) :
keyid = self . config . get ( fro . bare , ' ' , ' keys ' )
2011-11-12 04:19:06 +00:00
decrypted = self . gpg . decrypt ( ENCRYPTED_MESSAGE % { ' data ' : str ( encrypted . text ) } , passphrase = self . passphrase )
2011-11-12 02:44:12 +00:00
if not decrypted :
self . core . information ( ' Could not decrypt message from %s ' % ( fro . full ) , )
return
message [ ' body ' ] = str ( decrypted )
2011-11-12 04:19:06 +00:00
2011-11-12 04:48:29 +00:00
def display_encryption_status ( self , jid ) :
"""
Returns the status of encryption for the associated jid . This is to be used
in the ConversationTab ’ s InfoWin .
"""
if jid . full not in self . contacts . keys ( ) :
return ' '
2011-11-13 17:16:38 +00:00
status = self . contacts [ jid . full ]
2011-11-13 17:34:52 +00:00
if status in ( ' valid ' , ' invalid ' , ' signed ' ) :
2011-11-13 17:16:38 +00:00
return ' GPG Key: %s ( %s ) ' % ( status , ' encrypted ' if status == ' valid ' else ' NOT encrypted ' , )
else :
return ' GPG: Encryption %s ' % ( status , )
2011-11-12 04:48:29 +00:00
2011-11-12 04:59:46 +00:00
def command_gpg ( self , args ) :
2011-11-13 17:16:38 +00:00
"""
A command to force or disable the encryption , or to assign a keyid to a JID
"""
args = args . split ( )
if not args :
return self . core . command_help ( " gpg " )
if len ( args ) > = 2 :
jid = JID ( args [ 1 ] )
else :
if isinstance ( self . core . current_tab ( ) , ConversationTab ) :
jid = JID ( self . core . current_tab ( ) . get_name ( ) )
2012-01-29 02:34:01 +00:00
else :
return
2011-11-13 17:16:38 +00:00
command = args [ 0 ]
2011-11-13 17:34:52 +00:00
if command == ' force ' or command == ' enable ' :
2011-11-13 17:16:38 +00:00
# we can force encryption only with contact having an associated
# key, otherwise we cannot encrypt at all
if self . config . has_section ( ' keys ' ) and jid . bare in self . config . options ( ' keys ' ) :
self . contacts [ JID ( jid ) . full ] = ' forced '
else :
self . core . information ( ' Cannot force encryption: no key associated with %s ' % ( jid . bare ) , ' Info ' )
elif command == ' disable ' :
self . contacts [ JID ( jid ) . full ] = ' disabled '
2011-11-13 17:34:52 +00:00
elif command == ' setkey ' :
if len ( args ) != 3 :
return self . core . command_help ( " gpg " )
if not self . config . has_section ( ' keys ' ) :
self . config . add_section ( ' keys ' )
self . config . set ( jid . bare , args [ 2 ] , ' keys ' )
self . config . write ( )
2011-11-13 17:16:38 +00:00
self . core . refresh_window ( )
2011-11-13 17:34:52 +00:00
def gpg_completion ( self , the_input ) :
return the_input . auto_completion ( [ ' force ' , ' disable ' , ' setkey ' ] , ' ' )
2011-11-12 04:59:46 +00:00
2011-11-12 04:19:06 +00:00
def remove_gpg_headers ( self , text ) :
lines = text . splitlines ( )
while lines [ 0 ] . strip ( ) != ' ' :
lines . pop ( 0 )
while lines [ 0 ] . strip ( ) == ' ' :
lines . pop ( 0 )
res = [ ]
for line in lines :
if not line . startswith ( ' --- ' ) :
res . append ( line )
return ' \n ' . join ( res )