From 62cf72910f1b905cea18ee6db94c402fdca69a6c Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Mon, 11 Jun 2007 22:42:29 +0000 Subject: [PATCH] functioning XEP-0200 implementation (hardcoded keys and counters) --- src/chat_control.py | 16 +++- src/common/connection.py | 15 ++-- src/common/connection_handlers.py | 8 +- src/common/stanza_session.py | 137 +++++++++++++++++++++++++++++- src/common/xmpp/protocol.py | 1 + 5 files changed, 164 insertions(+), 13 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index c54fe026c..2df79545c 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1474,6 +1474,7 @@ class ChatControl(ChatControlBase): history_menuitem = xml.get_widget('history_menuitem') toggle_gpg_menuitem = xml.get_widget('toggle_gpg_menuitem') + toggle_e2e_menuitem = xml.get_widget('toggle_e2e_menuitem') add_to_roster_menuitem = xml.get_widget('add_to_roster_menuitem') send_file_menuitem = xml.get_widget('send_file_menuitem') compact_view_menuitem = xml.get_widget('compact_view_menuitem') @@ -1492,7 +1493,7 @@ class ChatControl(ChatControlBase): is_sensitive = gpg_btn.get_property('sensitive') toggle_gpg_menuitem.set_active(isactive) toggle_gpg_menuitem.set_property('sensitive', is_sensitive) - + # If we don't have resource, we can't do file transfer # in transports, contact holds our info we need to disable it too if contact.resource and contact.jid.find('@') != -1: @@ -1527,6 +1528,8 @@ class ChatControl(ChatControlBase): self.handlers[id] = add_to_roster_menuitem id = toggle_gpg_menuitem.connect('activate', self._on_toggle_gpg_menuitem_activate) + id = toggle_e2e_menuitem.connect('activate', + self._on_toggle_e2e_menuitem_activate) self.handlers[id] = toggle_gpg_menuitem id = information_menuitem.connect('activate', self._on_contact_information_menuitem_activate) @@ -1940,10 +1943,15 @@ class ChatControl(ChatControlBase): tb.set_active(not tb.get_active()) def _on_toggle_e2e_menuitem_activate(self, widget): - if 'security' in self.session.features and self.session.features['security'] == 'e2e': - self.session.negotiate_e2e() + #if 'security' in self.session.features and self.session.features['security'] == 'e2e': + if self.session.enable_encryption: + self.session.enable_encryption = False + print "e2e disabled." +# self.session.terminate_e2e() else: - self.session.terminate_e2e() + self.session.enable_encryption = True + print "e2e enabled." +# self.session.negotiate_e2e() def got_connected(self): ChatControlBase.got_connected(self) diff --git a/src/common/connection.py b/src/common/connection.py index 047e19d67..8e6eab6d0 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -885,11 +885,6 @@ class Connection(ConnectionHandlers): if msgenc: msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc) - # XEP-0201 - if session: - session.last_send = time.time() - msg_iq.setThread(session.thread_id) - # JEP-0172: user_nickname if user_nick: msg_iq.setTag('nick', namespace = common.xmpp.NS_NICK).setData( @@ -915,7 +910,17 @@ class Connection(ConnectionHandlers): if chatstate is 'composing' or msgtxt: chatstate_node.addChild(name = 'composing') + if session: + # XEP-0201 + session.last_send = time.time() + msg_iq.setThread(session.thread_id) + + # XEP-0200 + if session.enable_encryption: + msg_iq = session.encrypt_stanza(msg_iq) + self.connection.send(msg_iq) + no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')\ .split() ji = gajim.get_jid_without_resource(jid) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index f4f6306ff..9ea97d14c 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -37,7 +37,7 @@ from common import atom from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub -from common.stanza_session import StanzaSession +from common.stanza_session import EncryptedStanzaSession STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'error'] @@ -1440,6 +1440,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, common.xmpp.NS_FEATURE: self._FeatureNegCB(con, msg, session) return + + e2eTag = msg.getTag('c', namespace = common.xmpp.NS_STANZA_CRYPTO) + if e2eTag: + msg = session.decrypt_stanza(msg) msgtxt = msg.getBody() msghtml = msg.getXHTML() @@ -1611,7 +1615,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, return null_sessions[-1] def make_new_session(self, jid, thread_id = None, type = 'chat'): - sess = StanzaSession(self, jid, thread_id, type) + sess = EncryptedStanzaSession(self, jid, thread_id, type) if not jid in self.sessions: self.sessions[jid] = {} diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 98074c833..eb947fa55 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -6,6 +6,11 @@ from common import helpers import random import string +from Crypto.Cipher import AES +from Crypto.Hash import HMAC, SHA256 + +import base64 + class StanzaSession: def __init__(self, conn, jid, thread_id, type): self.conn = conn @@ -33,11 +38,139 @@ class StanzaSession: def generate_thread_id(self): return "".join([random.choice(string.letters) for x in xrange(0,32)]) + +class EncryptedStanzaSession(StanzaSession): + def __init__(self, conn, jid, thread_id, type = 'chat'): + StanzaSession.__init__(self, conn, jid, thread_id, type = 'chat') + + self.n = 128 + + self.cipher = AES + self.hash_alg = SHA256 + + self.en_key = '................' + self.de_key = '----------------' + + self.en_counter = 777 + self.de_counter = 777 ^ (2 ** (self.n - 1)) + + self.encrypter = self.cipher.new(self.en_key, self.cipher.MODE_CTR, counter=self.encryptcounter) + self.decrypter = self.cipher.new(self.de_key, self.cipher.MODE_CTR, counter=self.decryptcounter) + + self.compression = None + + self.enable_encryption = False + + # convert a large integer to a big-endian bitstring + def encode_mpi(self, n): + if n >= 256: + return self.encode_mpi(n / 256) + chr(n % 256) + else: + return chr(n) + + # convert a large integer to a big-endian bitstring, padded with \x00s to 16 bytes + def encode_mpi_with_padding(self, n): + ret = self.encode_mpi(n) + + mod = len(ret) % 16 + if mod != 0: + ret = ((16 - mod) * '\x00') + ret + + return ret + + # convert a big-endian bitstring to an integer + def decode_mpi(self, s): + if len(s) == 0: + return 0 + else: + return 256 * self.decode_mpi(s[:-1]) + ord(s[-1]) + + def encryptcounter(self): + self.en_counter = (self.en_counter + 1) % 2 ** self.n + return self.encode_mpi_with_padding(self.en_counter) + + def decryptcounter(self): + self.de_counter = (self.de_counter + 1) % 2 ** self.n + return self.encode_mpi_with_padding(self.de_counter) + + def encrypt_stanza(self, stanza): + encryptable = filter(lambda x: x.getName() not in ('error', 'amp', 'thread'), stanza.getChildren()) + + # XXX can also encrypt contents of elements in stanzas @type = 'error' + # (except for child elements) + + old_en_counter = self.en_counter + + for element in encryptable: + stanza.delChild(element) + + plaintext = ''.join(map(str, encryptable)) + + m_compressed = self.compress(plaintext) + m_final = self.encrypt(m_compressed) + + c = stanza.NT.c + c.setNamespace('http://www.xmpp.org/extensions/xep-0200.html#ns') + c.NT.data = base64.b64encode(m_final) + + # XXX check for rekey request, handle elements + + m_content = ''.join(map(str, c.getChildren())) + c.NT.mac = base64.b64encode(self.hmac(m_content, old_en_counter, self.en_key)) + + return stanza + + def hmac(self, content, counter, key): + return HMAC.new(key, content + self.encode_mpi_with_padding(counter), self.hash_alg).digest() + + def compress(self, plaintext): + if self.compression == None: + return plaintext + + def decompress(self, compressed): + if self.compression == None: + return compressed + + def encrypt(self, encryptable): + len_padding = 16 - (len(encryptable) % 16) + encryptable += len_padding * ' ' + + return self.encrypter.encrypt(encryptable) + + def decrypt_stanza(self, stanza): + c = stanza.getTag(name='c', namespace='http://www.xmpp.org/extensions/xep-0200.html#ns') + + stanza.delChild(c) + + # contents of , minus , minus whitespace + macable = ''.join(map(str, filter(lambda x: x.getName() != 'mac', c.getChildren()))) + + received_mac = base64.b64decode(c.getTagData('mac')) + calculated_mac = self.hmac(macable, self.de_counter, self.de_key) + + if not calculated_mac == received_mac: + raise 'bad signature (%s != %s)' % (repr(received_mac), repr(calculated_mac)) + + m_final = base64.b64decode(c.getTagData('data')) + m_compressed = self.decrypt(m_final) + plaintext = self.decompress(m_compressed) + + try: + parsed = xmpp.Node(node='' + plaintext + '') + except: + raise DecryptionError + + for child in parsed.getChildren(): + stanza.addChild(node=child) + + return stanza + + def decrypt(self, ciphertext): + return self.decrypter.decrypt(ciphertext) + def negotiate_e2e(): - pass - # # # urn:xmpp:ssn diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 6267e201a..7d903a292 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -81,6 +81,7 @@ NS_SESSION ='urn:ietf:params:xml:ns:xmpp-session' NS_SI ='http://jabber.org/protocol/si' # JEP-0096 NS_SI_PUB ='http://jabber.org/protocol/sipub' # JEP-0137 NS_SIGNED ='jabber:x:signed' # JEP-0027 +NS_STANZA_CRYPTO='http://www.xmpp.org/extensions/xep-0200.html#ns' # JEP-0200 NS_STANZAS ='urn:ietf:params:xml:ns:xmpp-stanzas' NS_STREAM ='http://affinix.com/jabber/stream' NS_STREAMS ='http://etherx.jabber.org/streams'