From 2e322cf221e90da9b9ba4f42eb290fd8dc36fee4 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Fri, 11 Nov 2011 23:44:26 +0100 Subject: Create an empty gpg plugin, including a gnupg wrapper. --- plugins/gpg/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 plugins/gpg/__init__.py (limited to 'plugins/gpg/__init__.py') diff --git a/plugins/gpg/__init__.py b/plugins/gpg/__init__.py new file mode 100644 index 00000000..88259acf --- /dev/null +++ b/plugins/gpg/__init__.py @@ -0,0 +1,8 @@ +from gpg import gnupg + +from plugin import BasePlugin + +class Plugin(BasePlugin): + def init(self): + pass + -- cgit v1.2.3 From 6b9d166e1cfe6c71a1f55d86e144a17fc3e73581 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 12 Nov 2011 02:48:13 +0100 Subject: Gpg module: send signed presences, and verify the signature in received presences. --- plugins/gpg/__init__.py | 70 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) (limited to 'plugins/gpg/__init__.py') diff --git a/plugins/gpg/__init__.py b/plugins/gpg/__init__.py index 88259acf..2c6d9981 100644 --- a/plugins/gpg/__init__.py +++ b/plugins/gpg/__init__.py @@ -1,8 +1,76 @@ from gpg import gnupg +from xml.etree import cElementTree as ET +import xml.sax.saxutils from plugin import BasePlugin +import logging +log = logging.getLogger(__name__) + +NS_SIGNED = "jabber:x:signed" +NS_ENCRYPTED = "jabber:x:encrypted" + class Plugin(BasePlugin): def init(self): - pass + self.contacts = {} + # a dict of {full-JID: 'signed'/'valid'/'invalid'} + # 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. + 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) + + def cleanup(self): + self.send_unsigned_presence() + + def sign_presence(self, presence): + """ + Sign every normal presence we send + """ + signed_element = ET.Element('{%s}x' % (NS_SIGNED)) + t = self.gpg.sign(presence['status'], keyid=self.keyid, passphrase=self.passphrase) + if not t: + self.core.information('Could not sign presence. Disabling GPG module', 'Info') + self.core.plugin_manager.unload('gpg') + return + signed_element.text = xml.sax.saxutils.escape(str(t)) + presence.append(signed_element) + + def send_unsigned_presence(self): + """ + Send our current presence, to everyone, but unsigned, to indicate + that we cannot/do not want to encrypt/unencrypt messages. + """ + current_presence = self.core.get_status() + self.core.command_status('%s %s' % (current_presence.show or 'available', current_presence.message,)) + 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: + log.debug('Not signed') + if bare in self.contacts.keys(): + del self.contacts[bare] + return + if self.config.has_section('keys') and bare in self.config.options('keys'): + verify = self.gpg.verify(signed.text) + if verify: + self.contacts[full] = 'valid' + else: + self.contacts[full] = 'invalid' + else: + self.contacts[full] = 'signed' -- cgit v1.2.3 From c2dfee141c7c6a3b082d6e1be69cef67c2704309 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 12 Nov 2011 03:44:12 +0100 Subject: GPG: encrypt and decrypt messages when possible. --- plugins/gpg/__init__.py | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) (limited to 'plugins/gpg/__init__.py') diff --git a/plugins/gpg/__init__.py b/plugins/gpg/__init__.py index 2c6d9981..055014dc 100644 --- a/plugins/gpg/__init__.py +++ b/plugins/gpg/__init__.py @@ -27,6 +27,8 @@ class Plugin(BasePlugin): self.add_event_handler('send_normal_presence', self.sign_presence) self.add_event_handler('normal_presence', self.on_normal_presence) + self.add_event_handler('conversation_say_after', self.on_conversation_say) + self.add_event_handler('conversation_msg', self.on_conversation_msg) def cleanup(self): self.send_unsigned_presence() @@ -35,7 +37,7 @@ class Plugin(BasePlugin): """ Sign every normal presence we send """ - signed_element = ET.Element('{%s}x' % (NS_SIGNED)) + signed_element = ET.Element('{%s}x' % (NS_SIGNED,)) t = self.gpg.sign(presence['status'], keyid=self.keyid, passphrase=self.passphrase) if not t: self.core.information('Could not sign presence. Disabling GPG module', 'Info') @@ -47,7 +49,7 @@ class Plugin(BasePlugin): def send_unsigned_presence(self): """ Send our current presence, to everyone, but unsigned, to indicate - that we cannot/do not want to encrypt/unencrypt messages. + that we cannot/do not want to encrypt/decrypt messages. """ current_presence = self.core.get_status() self.core.command_status('%s %s' % (current_presence.show or 'available', current_presence.message,)) @@ -74,3 +76,43 @@ class Plugin(BasePlugin): self.contacts[full] = 'invalid' else: self.contacts[full] = 'signed' + + 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 + log.debug('\n\n\n on_conversation_say: from: (%s). Contacts: %s' %(to, self.contacts,)) + signed = to.full in self.contacts.keys() + if signed: + veryfied = self.contacts[to.full] == 'valid' + 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,)) + encrypted_element.text = xml.sax.saxutils.escape(str(self.gpg.encrypt(message['body'], self.config.get(to.bare, '', section='keys')))) + message.append(encrypted_element) + message['body'] = 'This message has been encrypted.' + + 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'] + log.debug('\n\n\n--------- for message %s. ENCRYPTED: %s' % (message, encrypted,)) + 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') + decrypted = self.gpg.decrypt(encrypted.text, passphrase=self.passphrase) + if not decrypted: + self.core.information('Could not decrypt message from %s' % (fro.full),) + return + message['body'] = str(decrypted) -- cgit v1.2.3 From a97e6b548b32836e97e88e83fdb1d0073b15c8c1 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 12 Nov 2011 05:19:06 +0100 Subject: GPG now only send the encrypted data, not the full headers things. And it adds the headers to the encrypted data received, to decrypt it. --- plugins/gpg/__init__.py | 53 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 10 deletions(-) (limited to 'plugins/gpg/__init__.py') diff --git a/plugins/gpg/__init__.py b/plugins/gpg/__init__.py index 055014dc..a2742d3c 100644 --- a/plugins/gpg/__init__.py +++ b/plugins/gpg/__init__.py @@ -2,14 +2,35 @@ from gpg import gnupg from xml.etree import cElementTree as ET import xml.sax.saxutils -from plugin import BasePlugin - import logging log = logging.getLogger(__name__) +from plugin import BasePlugin + + NS_SIGNED = "jabber:x:signed" NS_ENCRYPTED = "jabber:x:encrypted" + +SIGNED_ATTACHED_MESSAGE = """-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA1 + +%(clear)s +-----BEGIN PGP SIGNATURE----- +Version: GnuPG + +%(data)s +-----END PGP SIGNATURE----- +""" + + +ENCRYPTED_MESSAGE = """-----BEGIN PGP MESSAGE----- +Version: GnuPG + +%(data)s +-----END PGP MESSAGE-----""" + + class Plugin(BasePlugin): def init(self): self.contacts = {} @@ -38,12 +59,13 @@ class Plugin(BasePlugin): Sign every normal presence we send """ signed_element = ET.Element('{%s}x' % (NS_SIGNED,)) - t = self.gpg.sign(presence['status'], keyid=self.keyid, passphrase=self.passphrase) + t = self.gpg.sign(presence['status'], keyid=self.keyid, passphrase=self.passphrase, detach=True) if not t: self.core.information('Could not sign presence. Disabling GPG module', 'Info') self.core.plugin_manager.unload('gpg') return - signed_element.text = xml.sax.saxutils.escape(str(t)) + text = xml.sax.saxutils.escape(str(t)) + signed_element.text = self.remove_gpg_headers(text) presence.append(signed_element) def send_unsigned_presence(self): @@ -64,12 +86,13 @@ class Plugin(BasePlugin): bare = presence['from'].bare full = presence['from'].full if signed is None: - log.debug('Not signed') if bare in self.contacts.keys(): del self.contacts[bare] return if self.config.has_section('keys') and bare in self.config.options('keys'): - verify = self.gpg.verify(signed.text) + to_verify = SIGNED_ATTACHED_MESSAGE % {'clear': presence['status'], + 'data': signed.text} + verify = self.gpg.verify(to_verify) if verify: self.contacts[full] = 'valid' else: @@ -86,7 +109,6 @@ class Plugin(BasePlugin): if not message['body']: # there’s nothing to encrypt if this is a chatstate, for example return - log.debug('\n\n\n on_conversation_say: from: (%s). Contacts: %s' %(to, self.contacts,)) signed = to.full in self.contacts.keys() if signed: veryfied = self.contacts[to.full] == 'valid' @@ -97,7 +119,7 @@ class Plugin(BasePlugin): # cannot be encrypted. del message['xhtml_im'] encrypted_element = ET.Element('{%s}x' % (NS_ENCRYPTED,)) - encrypted_element.text = xml.sax.saxutils.escape(str(self.gpg.encrypt(message['body'], self.config.get(to.bare, '', section='keys')))) + encrypted_element.text = self.remove_gpg_headers(xml.sax.saxutils.escape(str(self.gpg.encrypt(message['body'], self.config.get(to.bare, '', section='keys'))))) message.append(encrypted_element) message['body'] = 'This message has been encrypted.' @@ -107,12 +129,23 @@ class Plugin(BasePlugin): """ encrypted = message.find('{%s}x' % (NS_ENCRYPTED,)) fro = message['from'] - log.debug('\n\n\n--------- for message %s. ENCRYPTED: %s' % (message, encrypted,)) 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') - decrypted = self.gpg.decrypt(encrypted.text, passphrase=self.passphrase) + decrypted = self.gpg.decrypt(ENCRYPTED_MESSAGE % {'data': str(encrypted.text)}, passphrase=self.passphrase) if not decrypted: self.core.information('Could not decrypt message from %s' % (fro.full),) return message['body'] = str(decrypted) + + 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) -- cgit v1.2.3 From 9e8706a2e8bfb5dc1242ca42f87a6e3df90d9138 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 12 Nov 2011 05:48:29 +0100 Subject: =?UTF-8?q?a=20plugin=20can=20now=20add=20informations=20in=20Conv?= =?UTF-8?q?ersationTab=E2=80=99s=20InfoWin.=20And=20the=20GPG=20plugin=20d?= =?UTF-8?q?oes=20that.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/gpg/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'plugins/gpg/__init__.py') diff --git a/plugins/gpg/__init__.py b/plugins/gpg/__init__.py index a2742d3c..873aa285 100644 --- a/plugins/gpg/__init__.py +++ b/plugins/gpg/__init__.py @@ -7,6 +7,7 @@ log = logging.getLogger(__name__) from plugin import BasePlugin +from tabs import ConversationTab NS_SIGNED = "jabber:x:signed" NS_ENCRYPTED = "jabber:x:encrypted" @@ -51,8 +52,11 @@ class Plugin(BasePlugin): self.add_event_handler('conversation_say_after', self.on_conversation_say) self.add_event_handler('conversation_msg', self.on_conversation_msg) + ConversationTab.add_information_element('gpg', self.display_encryption_status) + def cleanup(self): self.send_unsigned_presence() + ConversationTab.remove_information_element('gpg') def sign_presence(self, presence): """ @@ -138,6 +142,15 @@ class Plugin(BasePlugin): return message['body'] = str(decrypted) + 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 '' + return ' GPG Key: %s' % self.contacts[jid.full] + def remove_gpg_headers(self, text): lines = text.splitlines() while lines[0].strip() != '': -- cgit v1.2.3 From 01e945a907e125b729599855636f8fc980312409 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 12 Nov 2011 05:59:46 +0100 Subject: =?UTF-8?q?Add=20a=20gpg=20command,=20doesn=E2=80=99t=20work=20yet?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/gpg/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'plugins/gpg/__init__.py') diff --git a/plugins/gpg/__init__.py b/plugins/gpg/__init__.py index 873aa285..f1b97575 100644 --- a/plugins/gpg/__init__.py +++ b/plugins/gpg/__init__.py @@ -35,12 +35,14 @@ Version: GnuPG class Plugin(BasePlugin): def init(self): self.contacts = {} - # a dict of {full-JID: 'signed'/'valid'/'invalid'} + # a dict of {full-JID: 'signed'/'valid'/'invalid'/'disabled'} # 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. + # 'disabled' means that the user do NOT want to encrypt its messages + # even if the key is valid. self.gpg = gnupg.GPG() self.keyid = self.config.get('keyid', '') or None self.passphrase = self.config.get('passphrase', '') or None @@ -52,11 +54,13 @@ class Plugin(BasePlugin): self.add_event_handler('conversation_say_after', self.on_conversation_say) self.add_event_handler('conversation_msg', self.on_conversation_msg) + self.add_tab_command(ConversationTab, 'gpg', self.command_gpg, "Usage: /gpg \nGpg: Force or disable gpg encryption with this fulljid.", lambda the_input: the_input.auto_completion(['force', 'disable'])) ConversationTab.add_information_element('gpg', self.display_encryption_status) def cleanup(self): self.send_unsigned_presence() ConversationTab.remove_information_element('gpg') + self.del_tab_command(ConversationTab, 'gpg') def sign_presence(self, presence): """ @@ -151,6 +155,10 @@ class Plugin(BasePlugin): return '' return ' GPG Key: %s' % self.contacts[jid.full] + def command_gpg(self, args): + # TODO + return + def remove_gpg_headers(self, text): lines = text.splitlines() while lines[0].strip() != '': -- cgit v1.2.3