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 ( )
self . core . command_status ( ' %s %s ' % ( current_presence . show or ' available ' , current_presence . message , ) )
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.
del message [ ' xhtml_im ' ]
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 )