From 9c61c2882fd1e3617dc6c7617a2da896596a4371 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 6 Apr 2012 15:13:04 -0400 Subject: Add support for XEP-0027 --- sleekxmpp/plugins/__init__.py | 1 + sleekxmpp/plugins/xep_0027/__init__.py | 15 +++ sleekxmpp/plugins/xep_0027/gpg.py | 161 +++++++++++++++++++++++++++++++++ sleekxmpp/plugins/xep_0027/stanza.py | 56 ++++++++++++ 4 files changed, 233 insertions(+) create mode 100644 sleekxmpp/plugins/xep_0027/__init__.py create mode 100644 sleekxmpp/plugins/xep_0027/gpg.py create mode 100644 sleekxmpp/plugins/xep_0027/stanza.py (limited to 'sleekxmpp/plugins') diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index c374f27b..6913a49a 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -18,6 +18,7 @@ __all__ = [ 'xep_0004', # Data Forms 'xep_0009', # Jabber-RPC 'xep_0012', # Last Activity + 'xep_0027', # Current Jabber OpenPGP Usage 'xep_0030', # Service Discovery 'xep_0033', # Extended Stanza Addresses 'xep_0045', # Multi-User Chat (Client) diff --git a/sleekxmpp/plugins/xep_0027/__init__.py b/sleekxmpp/plugins/xep_0027/__init__.py new file mode 100644 index 00000000..b6ed9676 --- /dev/null +++ b/sleekxmpp/plugins/xep_0027/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0027.stanza import Signed, Encrypted +from sleekxmpp.plugins.xep_0027.gpg import XEP_0027 + + +register_plugin(XEP_0027) diff --git a/sleekxmpp/plugins/xep_0027/gpg.py b/sleekxmpp/plugins/xep_0027/gpg.py new file mode 100644 index 00000000..ccc7c400 --- /dev/null +++ b/sleekxmpp/plugins/xep_0027/gpg.py @@ -0,0 +1,161 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.thirdparty import GPG + +from sleekxmpp.stanza import Presence, Message +from sleekxmpp.plugins.base import BasePlugin, register_plugin +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.xep_0027 import stanza, Signed, Encrypted + + +def _extract_data(data, kind): + stripped = [] + begin_headers = False + begin_data = False + for line in data.split('\n'): + if not begin_headers and 'BEGIN PGP %s' % kind in line: + begin_headers = True + continue + if begin_headers and line == '': + begin_data = True + continue + if 'END PGP %s' % kind in line: + return '\n'.join(stripped) + if begin_data: + stripped.append(line) + return '' + + +class XEP_0027(BasePlugin): + + name = 'xep_0027' + description = 'XEP-0027: Current Jabber OpenPGP Usage' + dependencies = set() + stanza = stanza + + def plugin_init(self): + self.gpg_binary = self.config.get('gpg_binary', 'gpg') + self.gpg_home = self.config.get('gpg_home', '') + self.use_agent = self.config.get('use_agent', True) + self.keyring = self.config.get('keyring', None) + self.key_server = self.config.get('key_server', 'pgp.mit.edu') + + self.gpg = GPG(gnupghome=self.gpg_home, + gpgbinary=self.gpg_binary, + use_agent=self.use_agent, + keyring=self.keyring) + + self.xmpp.add_filter('out', self._sign_presence) + + self._keyids = {} + + self.api.register(self._set_keyid, 'set_keyid', default=True) + self.api.register(self._get_keyid, 'get_keyid', default=True) + self.api.register(self._del_keyid, 'del_keyid', default=True) + self.api.register(self._get_keyids, 'get_keyids', default=True) + + register_stanza_plugin(Presence, Signed) + register_stanza_plugin(Message, Encrypted) + + self.xmpp.add_event_handler('unverified_signed_presence', + self._handle_unverified_signed_presence, + threaded=True) + + self.xmpp.register_handler( + Callback('Signed Presence', + StanzaPath('presence/signed'), + self._handle_signed_presence)) + + self.xmpp.register_handler( + Callback('Encrypted Message', + StanzaPath('message/encrypted'), + self._handle_encrypted_message)) + + def _sign_presence(self, stanza): + if isinstance(stanza, Presence): + if stanza['type'] == 'available' or stanza['type'] in Presence.showtypes: + stanza['signed'] = stanza['status'] + return stanza + + def sign(self, data, jid=None): + keyid = self.get_keyid(jid) + if keyid: + signed = self.gpg.sign(data, keyid=keyid) + return _extract_data(signed.data, 'SIGNATURE') + + def encrypt(self, data, jid=None): + keyid = self.get_keyid(jid) + if keyid: + enc = self.gpg.encrypt(data, keyid) + return _extract_data(enc.data, 'MESSAGE') + + def decrypt(self, data, jid=None): + template = '-----BEGIN PGP MESSAGE-----\n' + \ + '\n' + \ + '%s\n' + \ + '-----END PGP MESSAGE-----\n' + dec = self.gpg.decrypt(template % data) + return dec.data + + def verify(self, data, sig, jid=None): + template = '-----BEGIN PGP SIGNED MESSAGE-----\n' + \ + 'Hash: SHA1\n' + \ + '\n' + \ + '%s\n' + \ + '-----BEGIN PGP SIGNATURE-----\n' + \ + '\n' + \ + '%s\n' + \ + '-----END PGP SIGNATURE-----\n' + v = self.gpg.verify(template % (data, sig)) + return v + + def set_keyid(self, jid=None, keyid=None): + self.api['set_keyid'](jid, args=keyid) + + def get_keyid(self, jid=None): + return self.api['get_keyid'](jid) + + def del_keyid(self, jid=None): + self.api['del_keyid'](jid) + + def get_keyids(self): + return self.api['get_keyids']() + + def _handle_signed_presence(self, pres): + self.xmpp.event('unverified_signed_presence', pres) + + def _handle_unverified_signed_presence(self, pres): + verified = self.verify(pres['status'], pres['signed']) + if verified.key_id: + if not self.get_keyid(pres['from']): + known_keyids = [e['keyid'] for e in self.gpg.list_keys()] + if verified.key_id not in known_keyids: + self.gpg.recv_keys(self.key_server, verified.key_id) + self.set_keyid(jid=pres['from'], keyid=verified.key_id) + self.xmpp.event('signed_presence', pres) + + def _handle_encrypted_message(self, msg): + self.xmpp.event('encrypted_message', msg) + + # ================================================================= + + def _set_keyid(self, jid, node, ifrom, keyid): + self._keyids[jid] = keyid + + def _get_keyid(self, jid, node, ifrom, keyid): + return self._keyids.get(jid, None) + + def _del_keyid(self, jid, node, ifrom, keyid): + if jid in self._keyids: + del self._keyids[jid] + + def _get_keyids(self, jid, node, ifrom, data): + return self._keyids diff --git a/sleekxmpp/plugins/xep_0027/stanza.py b/sleekxmpp/plugins/xep_0027/stanza.py new file mode 100644 index 00000000..927693ad --- /dev/null +++ b/sleekxmpp/plugins/xep_0027/stanza.py @@ -0,0 +1,56 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase + + +class Signed(ElementBase): + name = 'x' + namespace = 'jabber:x:signed' + plugin_attrib = 'signed' + interfaces = set(['signed']) + is_extension = True + + def set_signed(self, value): + parent = self.parent() + xmpp = parent.stream + data = xmpp['xep_0027'].sign(value, parent['from']) + if data: + self.xml.text = data + else: + del parent['signed'] + + def get_signed(self): + return self.xml.text + + +class Encrypted(ElementBase): + name = 'x' + namespace = 'jabber:x:encrypted' + plugin_attrib = 'encrypted' + interfaces = set(['encrypted']) + is_extension = True + + def set_encrypted(self, value): + parent = self.parent() + xmpp = parent.stream + data = xmpp['xep_0027'].encrypt(value, parent['to'].bare) + if data: + self.xml.text = data + else: + del parent['encrypted'] + + def get_encrypted(self): + parent = self.parent() + xmpp = parent.stream + if self.xml.text: + return xmpp['xep_0027'].decrypt(self.xml.text, parent['to']) + return None + + + -- cgit v1.2.3