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'