functioning XEP-0200 implementation (hardcoded keys and counters)
This commit is contained in:
parent
4f2cd0a0fc
commit
62cf72910f
|
@ -1474,6 +1474,7 @@ class ChatControl(ChatControlBase):
|
||||||
|
|
||||||
history_menuitem = xml.get_widget('history_menuitem')
|
history_menuitem = xml.get_widget('history_menuitem')
|
||||||
toggle_gpg_menuitem = xml.get_widget('toggle_gpg_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')
|
add_to_roster_menuitem = xml.get_widget('add_to_roster_menuitem')
|
||||||
send_file_menuitem = xml.get_widget('send_file_menuitem')
|
send_file_menuitem = xml.get_widget('send_file_menuitem')
|
||||||
compact_view_menuitem = xml.get_widget('compact_view_menuitem')
|
compact_view_menuitem = xml.get_widget('compact_view_menuitem')
|
||||||
|
@ -1492,7 +1493,7 @@ class ChatControl(ChatControlBase):
|
||||||
is_sensitive = gpg_btn.get_property('sensitive')
|
is_sensitive = gpg_btn.get_property('sensitive')
|
||||||
toggle_gpg_menuitem.set_active(isactive)
|
toggle_gpg_menuitem.set_active(isactive)
|
||||||
toggle_gpg_menuitem.set_property('sensitive', is_sensitive)
|
toggle_gpg_menuitem.set_property('sensitive', is_sensitive)
|
||||||
|
|
||||||
# If we don't have resource, we can't do file transfer
|
# If we don't have resource, we can't do file transfer
|
||||||
# in transports, contact holds our info we need to disable it too
|
# in transports, contact holds our info we need to disable it too
|
||||||
if contact.resource and contact.jid.find('@') != -1:
|
if contact.resource and contact.jid.find('@') != -1:
|
||||||
|
@ -1527,6 +1528,8 @@ class ChatControl(ChatControlBase):
|
||||||
self.handlers[id] = add_to_roster_menuitem
|
self.handlers[id] = add_to_roster_menuitem
|
||||||
id = toggle_gpg_menuitem.connect('activate',
|
id = toggle_gpg_menuitem.connect('activate',
|
||||||
self._on_toggle_gpg_menuitem_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
|
self.handlers[id] = toggle_gpg_menuitem
|
||||||
id = information_menuitem.connect('activate',
|
id = information_menuitem.connect('activate',
|
||||||
self._on_contact_information_menuitem_activate)
|
self._on_contact_information_menuitem_activate)
|
||||||
|
@ -1940,10 +1943,15 @@ class ChatControl(ChatControlBase):
|
||||||
tb.set_active(not tb.get_active())
|
tb.set_active(not tb.get_active())
|
||||||
|
|
||||||
def _on_toggle_e2e_menuitem_activate(self, widget):
|
def _on_toggle_e2e_menuitem_activate(self, widget):
|
||||||
if 'security' in self.session.features and self.session.features['security'] == 'e2e':
|
#if 'security' in self.session.features and self.session.features['security'] == 'e2e':
|
||||||
self.session.negotiate_e2e()
|
if self.session.enable_encryption:
|
||||||
|
self.session.enable_encryption = False
|
||||||
|
print "e2e disabled."
|
||||||
|
# self.session.terminate_e2e()
|
||||||
else:
|
else:
|
||||||
self.session.terminate_e2e()
|
self.session.enable_encryption = True
|
||||||
|
print "e2e enabled."
|
||||||
|
# self.session.negotiate_e2e()
|
||||||
|
|
||||||
def got_connected(self):
|
def got_connected(self):
|
||||||
ChatControlBase.got_connected(self)
|
ChatControlBase.got_connected(self)
|
||||||
|
|
|
@ -885,11 +885,6 @@ class Connection(ConnectionHandlers):
|
||||||
if msgenc:
|
if msgenc:
|
||||||
msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(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
|
# JEP-0172: user_nickname
|
||||||
if user_nick:
|
if user_nick:
|
||||||
msg_iq.setTag('nick', namespace = common.xmpp.NS_NICK).setData(
|
msg_iq.setTag('nick', namespace = common.xmpp.NS_NICK).setData(
|
||||||
|
@ -915,7 +910,17 @@ class Connection(ConnectionHandlers):
|
||||||
if chatstate is 'composing' or msgtxt:
|
if chatstate is 'composing' or msgtxt:
|
||||||
chatstate_node.addChild(name = 'composing')
|
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)
|
self.connection.send(msg_iq)
|
||||||
|
|
||||||
no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')\
|
no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')\
|
||||||
.split()
|
.split()
|
||||||
ji = gajim.get_jid_without_resource(jid)
|
ji = gajim.get_jid_without_resource(jid)
|
||||||
|
|
|
@ -37,7 +37,7 @@ from common import atom
|
||||||
from common.commands import ConnectionCommands
|
from common.commands import ConnectionCommands
|
||||||
from common.pubsub import ConnectionPubSub
|
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',
|
STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
|
||||||
'invisible', 'error']
|
'invisible', 'error']
|
||||||
|
@ -1440,6 +1440,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
||||||
common.xmpp.NS_FEATURE:
|
common.xmpp.NS_FEATURE:
|
||||||
self._FeatureNegCB(con, msg, session)
|
self._FeatureNegCB(con, msg, session)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
e2eTag = msg.getTag('c', namespace = common.xmpp.NS_STANZA_CRYPTO)
|
||||||
|
if e2eTag:
|
||||||
|
msg = session.decrypt_stanza(msg)
|
||||||
|
|
||||||
msgtxt = msg.getBody()
|
msgtxt = msg.getBody()
|
||||||
msghtml = msg.getXHTML()
|
msghtml = msg.getXHTML()
|
||||||
|
@ -1611,7 +1615,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
||||||
return null_sessions[-1]
|
return null_sessions[-1]
|
||||||
|
|
||||||
def make_new_session(self, jid, thread_id = None, type = 'chat'):
|
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:
|
if not jid in self.sessions:
|
||||||
self.sessions[jid] = {}
|
self.sessions[jid] = {}
|
||||||
|
|
|
@ -6,6 +6,11 @@ from common import helpers
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Hash import HMAC, SHA256
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
class StanzaSession:
|
class StanzaSession:
|
||||||
def __init__(self, conn, jid, thread_id, type):
|
def __init__(self, conn, jid, thread_id, type):
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
|
@ -33,11 +38,139 @@ class StanzaSession:
|
||||||
def generate_thread_id(self):
|
def generate_thread_id(self):
|
||||||
return "".join([random.choice(string.letters) for x in xrange(0,32)])
|
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 <error/> elements in stanzas @type = 'error'
|
||||||
|
# (except for <defined-condition xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> 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 <key/> 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 <c>, minus <mac>, 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='<node>' + plaintext + '</node>')
|
||||||
|
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():
|
def negotiate_e2e():
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
#<x type='form' xmlns='jabber:x:data'>
|
#<x type='form' xmlns='jabber:x:data'>
|
||||||
# <field type='hidden' var='FORM_TYPE'>
|
# <field type='hidden' var='FORM_TYPE'>
|
||||||
# <value>urn:xmpp:ssn</value>
|
# <value>urn:xmpp:ssn</value>
|
||||||
|
|
|
@ -81,6 +81,7 @@ NS_SESSION ='urn:ietf:params:xml:ns:xmpp-session'
|
||||||
NS_SI ='http://jabber.org/protocol/si' # JEP-0096
|
NS_SI ='http://jabber.org/protocol/si' # JEP-0096
|
||||||
NS_SI_PUB ='http://jabber.org/protocol/sipub' # JEP-0137
|
NS_SI_PUB ='http://jabber.org/protocol/sipub' # JEP-0137
|
||||||
NS_SIGNED ='jabber:x:signed' # JEP-0027
|
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_STANZAS ='urn:ietf:params:xml:ns:xmpp-stanzas'
|
||||||
NS_STREAM ='http://affinix.com/jabber/stream'
|
NS_STREAM ='http://affinix.com/jabber/stream'
|
||||||
NS_STREAMS ='http://etherx.jabber.org/streams'
|
NS_STREAMS ='http://etherx.jabber.org/streams'
|
||||||
|
|
Loading…
Reference in New Issue