[Brendan Taylor] Gsoc 2007 work : end to end encryptions. Fixes #544

This commit is contained in:
Yann Leboulanger 2007-08-25 22:42:35 +00:00
commit c9a407ca52
19 changed files with 1809 additions and 78 deletions

View File

@ -35,7 +35,7 @@
<child internal-child="image">
<widget class="GtkImage" id="image1371">
<property name="visible">True</property>
<property name="stock">gtk-missing-image</property>
<property name="stock">gtk-file</property>
<property name="icon_size">1</property>
</widget>
</child>
@ -49,6 +49,15 @@
<signal name="activate" handler="_on_toggle_gpg_menuitem_activate"/>
</widget>
</child>
<child>
<widget class="GtkCheckMenuItem" id="toggle_e2e_menuitem">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Toggle End to End Encryption</property>
<property name="use_underline">True</property>
<signal name="activate" handler="_on_toggle_e2e_menuitem_activate"/>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="add_to_roster_menuitem">
<property name="visible">True</property>

View File

@ -953,11 +953,11 @@ class ChatControl(ChatControlBase):
TYPE_ID = message_control.TYPE_CHAT
old_msg_kind = None # last kind of the printed message
CHAT_CMDS = ['clear', 'compact', 'help', 'me', 'ping', 'say']
def __init__(self, parent_win, contact, acct, resource = None):
def __init__(self, parent_win, contact, acct, session, resource = None):
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
'chat_child_vbox', contact, acct, resource)
# for muc use:
# widget = self.xml.get_widget('muc_window_actions_button')
widget = self.xml.get_widget('message_window_actions_button')
@ -973,7 +973,7 @@ class ChatControl(ChatControlBase):
# it is on enter-notify and leave-notify so no need to be per jid
self.show_bigger_avatar_timeout_id = None
self.bigger_avatar_window = None
self.show_avatar(self.contact.resource)
self.show_avatar(self.contact.resource)
# chatstate timers and state
self.reset_kbd_mouse_timeout_vars()
@ -987,7 +987,7 @@ class ChatControl(ChatControlBase):
id = message_tv_buffer.connect('changed',
self._on_message_tv_buffer_changed)
self.handlers[id] = message_tv_buffer
widget = self.xml.get_widget('avatar_eventbox')
id = widget.connect('enter-notify-event',
self.on_avatar_eventbox_enter_notify_event)
@ -1007,7 +1007,12 @@ class ChatControl(ChatControlBase):
if self.contact.jid in gajim.encrypted_chats[self.account]:
self.xml.get_widget('gpg_togglebutton').set_active(True)
self.session = session
# does this window have an existing, active esession?
self.esessioned = False
self.status_tooltip = gtk.Tooltips()
self.update_ui()
# restore previous conversation
@ -1300,13 +1305,13 @@ class ChatControl(ChatControlBase):
contact = self.contact
encrypted = bool(self.session) and self.session.enable_encryption
keyID = ''
encrypted = False
if self.xml.get_widget('gpg_togglebutton').get_active():
keyID = contact.keyID
encrypted = True
chatstates_on = gajim.config.get('outgoing_chat_state_notifications') != \
'disabled'
composing_xep = contact.composing_xep
@ -1318,7 +1323,7 @@ class ChatControl(ChatControlBase):
# this is here (and not in send_chatstate)
# because we want it sent with REAL message
# (not standlone) eg. one that has body
if contact.our_chatstate:
# We already asked for xep 85, don't ask it twice
composing_xep = 'asked_once'
@ -1417,18 +1422,46 @@ class ChatControl(ChatControlBase):
kind = 'info'
name = ''
else:
ec = gajim.encrypted_chats[self.account]
if encrypted and jid not in ec:
msg = _('Encryption enabled')
# ESessions
if self.session and self.session.enable_encryption:
if not self.esessioned:
msg = _('Encryption enabled')
ChatControlBase.print_conversation_line(self, msg,
'status', '', tim)
if self.session.loggable:
msg = _('Session WILL be logged')
else:
msg = _('Session WILL NOT be logged')
ChatControlBase.print_conversation_line(self, msg,
'status', '', tim)
self.esessioned = True
elif not encrypted:
msg = _('The following message was NOT encrypted')
ChatControlBase.print_conversation_line(self, msg,
'status', '', tim)
elif self.esessioned:
msg = _('Encryption disabled')
ChatControlBase.print_conversation_line(self, msg,
'status', '', tim)
ec.append(jid)
elif not encrypted and jid in ec:
msg = _('Encryption disabled')
ChatControlBase.print_conversation_line(self, msg,
'status', '', tim)
ec.remove(jid)
self.xml.get_widget('gpg_togglebutton').set_active(encrypted)
self.esessioned = False
else:
# GPG encryption
ec = gajim.encrypted_chats[self.account]
if encrypted and jid not in ec:
msg = _('Encryption enabled')
ChatControlBase.print_conversation_line(self, msg,
'status', '', tim)
ec.append(jid)
elif not encrypted and jid in ec:
msg = _('Encryption disabled')
ChatControlBase.print_conversation_line(self, msg,
'status', '', tim)
ec.remove(jid)
self.xml.get_widget('gpg_togglebutton').set_active(encrypted)
if not frm:
kind = 'incoming'
name = contact.get_shown_name()
@ -1538,6 +1571,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')
information_menuitem = xml.get_widget('information_menuitem')
@ -1557,6 +1591,10 @@ class ChatControl(ChatControlBase):
toggle_gpg_menuitem.set_active(isactive)
toggle_gpg_menuitem.set_property('sensitive', is_sensitive)
# TODO: check that the remote client supports e2e
isactive = int(self.session != None and self.session.enable_encryption)
toggle_e2e_menuitem.set_active(isactive)
# 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 self.TYPE_ID == message_control.TYPE_PM and self.gc_contact.jid and \
@ -1591,6 +1629,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)
@ -1908,6 +1948,9 @@ class ChatControl(ChatControlBase):
encrypted = data[4], subject = data[1], xhtml = data[7])
if len(data) > 6 and isinstance(data[6], int):
message_ids.append(data[6])
if len(data) > 8:
self.set_session(data[8])
if message_ids:
gajim.logger.set_read_messages(message_ids)
gajim.events.remove_events(self.account, jid_with_resource,
@ -2035,6 +2078,28 @@ class ChatControl(ChatControlBase):
'''user want to invite some friends to chat'''
dialogs.TransformChatToMUC(self.account, [self.contact.jid])
def _on_toggle_e2e_menuitem_activate(self, widget):
if self.session and self.session.enable_encryption:
self.session.terminate_e2e()
msg = _('Encryption disabled')
ChatControlBase.print_conversation_line(self, msg,
'status', '', None)
self.esessioned = False
jid = str(self.session.jid)
gajim.connections[self.account].delete_session(jid,
self.session.thread_id)
self.session = gajim.connections[self.account].make_new_session(jid)
else:
if not self.session:
self.session = gajim.connections[self.account].make_new_session(
self.contact.jid)
# XXX decide whether to use 4 or 3 message negotiation
self.session.negotiate_e2e(False)
def got_connected(self):
ChatControlBase.got_connected(self)

View File

@ -122,7 +122,7 @@ def check_and_possibly_create_paths():
print _('%s is a directory but should be a file') % LOG_DB_PATH
print _('Gajim will now exit')
sys.exit()
else: # dot_gajim doesn't exist
if dot_gajim: # is '' on win9x so avoid that
create_path(dot_gajim)

View File

@ -175,6 +175,7 @@ class Config:
'tabs_always_visible': [opt_bool, False, _('Show tab when only one conversation?')],
'tabs_border': [opt_bool, False, _('Show tabbed notebook border in chat windows?')],
'tabs_close_button': [opt_bool, True, _('Show close button in tab?')],
'log_encrypted_sessions': [opt_bool, False, _('When negotiating an encrypted session, should Gajim assume you want your messages to be logged?')],
'chat_avatar_width': [opt_int, 52],
'chat_avatar_height': [opt_int, 52],
'roster_avatar_width': [opt_int, 32],

View File

@ -105,6 +105,7 @@ class ConfigPaths:
def init_profile(self, profile = ''):
conffile = windowsify(u'config')
pidfile = windowsify(u'gajim')
secretsfile = windowsify(u'secrets')
if len(profile) > 0:
conffile += u'.' + profile
@ -112,6 +113,7 @@ class ConfigPaths:
pidfile += u'.pid'
self.add_from_root('CONFIG_FILE', conffile)
self.add_from_root('PID_FILE', pidfile)
self.add_from_root('SECRETS_FILE', secretsfile)
# for k, v in paths.iteritems():
# print "%s: %s" % (repr(k), repr(v))

View File

@ -22,6 +22,8 @@ import os
import random
import socket
import time
try:
randomsource = random.SystemRandom()
except:
@ -819,7 +821,7 @@ class Connection(ConnectionHandlers):
def send_message(self, jid, msg, keyID, type = 'chat', subject='',
chatstate = None, msg_id = None, composing_xep = None, resource = None,
user_nick = None, xhtml = None, forward_from = None):
user_nick = None, xhtml = None, session = None, forward_from = None):
if not self.connection:
return 1
if msg and not xhtml and gajim.config.get('rst_formatting_outgoing_messages'):
@ -868,7 +870,7 @@ class Connection(ConnectionHandlers):
msg_iq.setTag('nick', namespace = common.xmpp.NS_NICK).setData(
user_nick)
# chatstates - if peer supports jep85 or jep22, send chatstates
# chatstates - if peer supports xep85 or xep22, send chatstates
# please note that the only valid tag inside a message containing a <body>
# tag is the active event
if chatstate is not None:
@ -893,6 +895,15 @@ class Connection(ConnectionHandlers):
namespace=common.xmpp.NS_ADDRESS)
addresses.addChild('address', attrs = {'type': 'ofrom',
'jid': forward_from})
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)
if not forward_from:
no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')\
@ -912,6 +923,18 @@ class Connection(ConnectionHandlers):
except exceptions.PysqliteOperationalError, e:
self.dispatch('ERROR', (_('Disk Write Error'), str(e)))
self.dispatch('MSGSENT', (jid, msg, keyID))
if session.is_loggable():
log_msg = msg
if subject:
log_msg = _('Subject: %s\n%s') % (subject, msg)
if log_msg:
if type == 'chat':
kind = 'chat_msg_sent'
else:
kind = 'single_msg_sent'
gajim.logger.write(kind, jid, log_msg)
self.dispatch('MSGSENT', (jid, msg, keyID))
def send_stanza(self, stanza):
''' send a stanza untouched '''

View File

@ -24,7 +24,7 @@ import socket
import sys
from time import (altzone, daylight, gmtime, localtime, mktime, strftime,
time as time_time, timezone, tzname)
time as time_time, timezone, tzname)
from calendar import timegm
import socks5
@ -39,6 +39,8 @@ from common.commands import ConnectionCommands
from common.pubsub import ConnectionPubSub
from common.caps import ConnectionCaps
from common.stanza_session import EncryptedStanzaSession
STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
'invisible', 'error']
# kind of events we can wait for an answer
@ -57,7 +59,7 @@ except:
class ConnectionBytestream:
def __init__(self):
self.files_props = {}
def is_transfer_stoped(self, file_props):
if file_props.has_key('error') and file_props['error'] != 0:
return True
@ -346,7 +348,7 @@ class ConnectionBytestream:
iq.setID(auth_id)
query = iq.setTag('query')
query.setNamespace(common.xmpp.NS_BYTESTREAM)
query.setAttr('sid', proxy['sid'])
query.setAttr('sid', proxy['sid'])
activate = query.setTag('activate')
activate.setData(file_props['proxy_receiver'])
iq.setID(auth_id)
@ -437,7 +439,7 @@ class ConnectionBytestream:
gajim.proxy65_manager.resolve_result(frm, query)
try:
streamhost = query.getTag('streamhost-used')
streamhost = query.getTag('streamhost-used')
except: # this bytestream result is not what we need
pass
id = real_id[3:]
@ -703,7 +705,7 @@ class ConnectionDisco:
def _DiscoverItemsGetCB(self, con, iq_obj):
gajim.log.debug('DiscoverItemsGetCB')
node = iq_obj.getTagAttr('query', 'node')
if node is None:
if node is None:
result = iq_obj.buildReply('result')
self.connection.send(result)
raise common.xmpp.NodeProcessed
@ -738,6 +740,7 @@ class ConnectionDisco:
q.addChild('feature', attrs = {'var': common.xmpp.NS_MUC})
q.addChild('feature', attrs = {'var': common.xmpp.NS_COMMANDS})
q.addChild('feature', attrs = {'var': common.xmpp.NS_DISCO_INFO})
q.addChild('feature', attrs = {'var': common.xmpp.NS_ESESSION_INIT})
if (node is None or extension == 'cstates') and gajim.config.get('outgoing_chat_state_notifactions') != 'disabled':
q.addChild('feature', attrs = {'var': common.xmpp.NS_CHATSTATES})
@ -779,12 +782,12 @@ class ConnectionDisco:
for key in i.getAttrs().keys():
attr[key] = i.getAttr(key)
if attr.has_key('category') and \
attr['category'] in ('gateway', 'headline') and \
attr.has_key('type'):
attr['category'] in ('gateway', 'headline') and \
attr.has_key('type'):
transport_type = attr['type']
if attr.has_key('category') and \
attr['category'] == 'conference' and \
attr.has_key('type') and attr['type'] == 'text':
attr['category'] == 'conference' and \
attr.has_key('type') and attr['type'] == 'text':
is_muc = True
identities.append(attr)
elif i.getName() == 'feature':
@ -1193,14 +1196,17 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
# keep the jids we auto added (transports contacts) to not send the
# SUBSCRIBED event to gui
self.automatically_added = []
# keep the latest subscribed event for each jid to prevent loop when we
# keep the latest subscribed event for each jid to prevent loop when we
# acknoledge presences
self.subscribed_events = {}
# keep track of sessions this connection has with other JIDs
self.sessions = {}
try:
idle.init()
except:
HAS_IDLE = False
def build_http_auth_answer(self, iq_obj, answer):
if answer == 'yes':
self.connection.send(iq_obj.buildReply('result'))
@ -1222,6 +1228,29 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
self.dispatch('HTTP_AUTH', (method, url, id, iq_obj, msg));
raise common.xmpp.NodeProcessed
def _FeatureNegCB(self, con, stanza, session):
gajim.log.debug('FeatureNegCB')
feature = stanza.getTag(name='feature', namespace=common.xmpp.NS_FEATURE)
form = common.xmpp.DataForm(node=feature.getTag('x'))
if form['FORM_TYPE'] == 'urn:xmpp:ssn':
self.dispatch('SESSION_NEG', (stanza.getFrom(), session, form))
else:
reply = stanza.buildReply()
reply.setType('error')
reply.addChild(feature)
reply.addChild(node=xmpp.ErrorNode('service-unavailable', typ='cancel'))
con.send(reply)
def _InitE2ECB(self, con, stanza, session):
gajim.log.debug('InitE2ECB')
init = stanza.getTag(name='init', namespace=common.xmpp.NS_ESESSION_INIT)
form = common.xmpp.DataForm(node=init.getTag('x'))
self.dispatch('SESSION_NEG', (stanza.getFrom(), session, form))
def _ErrorCB(self, con, iq_obj):
gajim.log.debug('ErrorCB')
if iq_obj.getQueryNS() == common.xmpp.NS_VERSION:
@ -1298,10 +1327,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
def _rosterSetCB(self, con, iq_obj):
gajim.log.debug('rosterSetCB')
for item in iq_obj.getTag('query').getChildren():
jid = helpers.parse_jid(item.getAttr('jid'))
jid = helpers.parse_jid(item.getAttr('jid'))
name = item.getAttr('name')
sub = item.getAttr('subscription')
ask = item.getAttr('ask')
sub = item.getAttr('subscription')
ask = item.getAttr('ask')
groups = []
for group in item.getTags('group'):
groups.append(group.getData())
@ -1384,7 +1413,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
gajim.log.debug('TimeRevisedCB')
iq_obj = iq_obj.buildReply('result')
qp = iq_obj.setTag('time',
namespace=common.xmpp.NS_TIME_REVISED)
namespace=common.xmpp.NS_TIME_REVISED)
qp.setTagData('utc', strftime('%Y-%m-%dT%TZ', gmtime()))
zone = -(timezone, altzone)[daylight] / 60
tzo = (zone / 60, abs(zone % 60))
@ -1437,6 +1466,19 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
def _messageCB(self, con, msg):
'''Called when we receive a message'''
frm = helpers.get_full_jid_from_iq(msg)
mtype = msg.getType()
thread_id = msg.getThread()
if not mtype:
mtype = 'normal'
if not mtype == 'groupchat':
session = self.get_session(frm, thread_id, mtype)
if thread_id and not session.received_thread_id:
session.received_thread_id = True
# check if the message is pubsub#event
if msg.getTag('event') is not None:
self._pubsubEventCB(con, msg)
@ -1446,9 +1488,30 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
common.xmpp.NS_HTTP_AUTH:
self._HttpAuthCB(con, msg)
return
if msg.getTag('feature') and msg.getTag('feature').namespace == \
common.xmpp.NS_FEATURE:
self._FeatureNegCB(con, msg, session)
return
if msg.getTag('init') and msg.getTag('init').namespace == \
common.xmpp.NS_ESESSION_INIT:
self._InitE2ECB(con, msg, session)
encrypted = False
tim = msg.getTimestamp()
tim = strptime(tim, '%Y%m%dT%H:%M:%S')
tim = localtime(timegm(tim))
e2e_tag = msg.getTag('c', namespace = common.xmpp.NS_STANZA_CRYPTO)
if e2e_tag:
encrypted = True
try:
msg = session.decrypt_stanza(msg)
except:
self.dispatch('FAILED_DECRYPT', (frm, tim))
msgtxt = msg.getBody()
msghtml = msg.getXHTML()
mtype = msg.getType()
subject = msg.getSubject() # if not there, it's None
tim = msg.getTimestamp()
tim = helpers.datetime_tuple(tim)
@ -1518,7 +1581,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
if encTag and GnuPG.USE_GPG:
#decrypt
encmsg = encTag.getData()
keyID = gajim.config.get_per('accounts', self.name, 'keyid')
if keyID:
decmsg = self.gpg.decrypt(encmsg, keyID)
@ -1530,7 +1593,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
if not error_msg:
error_msg = msgtxt
msgtxt = None
if self.name not in no_log_for:
if session.is_loggable():
try:
gajim.logger.write('error', frm, error_msg, tim = tim,
subject = subject)
@ -1568,8 +1631,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
elif mtype == 'chat': # it's type 'chat'
if not msg.getTag('body') and chatstate is None: #no <body>
return
if msg.getTag('body') and self.name not in no_log_for and jid not in\
no_log_for and msgtxt:
if msg.getTag('body') and session.is_loggable() and msgtxt:
try:
msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt,
tim = tim, subject = subject)
@ -1588,7 +1650,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
self.dispatch('GC_INVITATION',(frm, jid_from, reason, password,
is_continued))
return
if self.name not in no_log_for and jid not in no_log_for and msgtxt:
if session.is_loggable()and msgtxt:
try:
gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim,
subject = subject)
@ -1599,9 +1661,81 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
if treat_as:
mtype = treat_as
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype,
subject, chatstate, msg_id, composing_xep, user_nick, msghtml))
subject, chatstate, msg_id, composing_xep, user_nick, msghtml,
session))
# END messageCB
def get_session(self, jid, thread_id, type):
'''returns an existing session between this connection and 'jid', returns a new one if none exist.'''
session = self.find_session(jid, thread_id, type)
if session:
return session
else:
# it's possible we initiated a session with a bare JID and this is the
# first time we've seen a resource
bare_jid = gajim.get_jid_without_resource(jid)
if bare_jid != jid:
session = self.find_session(bare_jid, thread_id, type)
if session:
if not session.received_thread_id:
thread_id = session.thread_id
self.move_session(bare_jid, thread_id, jid.split("/")[1])
return session
return self.make_new_session(jid, thread_id, type)
def find_session(self, jid, thread_id, type):
try:
if type == 'chat' and not thread_id:
return self.find_null_session(jid)
else:
return self.sessions[jid][thread_id]
except KeyError:
return None
def delete_session(self, jid, thread_id):
del self.sessions[jid][thread_id]
if not self.sessions[jid]:
del self.sessions[jid]
def move_session(self, original_jid, thread_id, to_resource):
'''moves a session to another resource.'''
session = self.sessions[original_jid][thread_id]
del self.sessions[original_jid][thread_id]
new_jid = gajim.get_jid_without_resource(original_jid) + '/' + to_resource
session.jid = common.xmpp.JID(new_jid)
if not new_jid in self.sessions:
self.sessions[new_jid] = {}
self.sessions[new_jid][thread_id] = session
def find_null_session(self, jid):
'''finds all of the sessions between us and jid that jid hasn't sent a thread_id in yet.
returns the session that we last sent a message to.'''
sessions_with_jid = self.sessions[jid].values()
no_threadid_sessions = filter(lambda s: not s.received_thread_id, sessions_with_jid)
no_threadid_sessions.sort(key=lambda s: s.last_send)
return no_threadid_sessions[-1]
def make_new_session(self, jid, thread_id = None, type = 'chat'):
sess = EncryptedStanzaSession(self, common.xmpp.JID(jid), thread_id, type)
if not jid in self.sessions:
self.sessions[jid] = {}
self.sessions[jid][sess.thread_id] = sess
return sess
def _pubsubEventCB(self, con, msg):
''' Called when we receive <message/> with pubsub event. '''
# TODO: Logging? (actually services where logging would be useful, should

207
src/common/dh.py Normal file
View File

@ -0,0 +1,207 @@
import string
# This file defines a number of constants; specifically, large primes suitable for
# use with the Diffie-Hellman key exchange.
#
# These constants have been obtained from RFC2409 and RFC3526.
generators = [ None, # one to get the right offset
2,
2,
None,
None,
2,
None,
None,
None,
None,
None,
None,
None,
None,
2, # group 14
2,
2,
2,
2,
]
hex_primes = [ None,
# group 1
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A63A3620 FFFFFFFF FFFFFFFF''',
# group 2
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE65381
FFFFFFFF FFFFFFFF''',
# XXX how do I obtain these?
None,
None,
# group 5
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA237327 FFFFFFFF FFFFFFFF''',
None,
None,
None,
None,
None,
None,
None,
None,
# group 14
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AACAA68 FFFFFFFF FFFFFFFF''',
# group 15
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64
ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B
F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31
43DB5BFC E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF''',
# group 16
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64
ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B
F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31
43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7
88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA
2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6
287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED
1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9
93B4EA98 8D8FDDC1 86FFFB7DC 90A6C08F 4DF435C9 34063199
FFFFFFFF FFFFFFFF''',
# group 17
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08
8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B
302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9
A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6
49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8
FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C
180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718
3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D
04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D
B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226
1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC
E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26
99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB
04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2
233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127
D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492
36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406
AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918
DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151
2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03
F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F
BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA
CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B
B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632
387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E
6DCC4024 FFFFFFFF FFFFFFFF''',
# group 18
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64
ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B
F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31
43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7
88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA
2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6
287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED
1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9
93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492
36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD
F8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831
179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B
DB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF
5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6
D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3
23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA
CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328
06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C
DA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE
12BF2D5B 0B7474D6 E694F91E 6DBE1159 74A3926F 12FEE5E4
38777CB6 A932DF8C D8BEC4D0 73B931BA 3BC832B6 8D9DD300
741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C 5AE4F568
3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9
22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B
4BCBC886 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A
062B3CF5 B3A278A6 6D2A13F8 3F44F82D DF310EE0 74AB6A36
4597E899 A0255DC1 64F31CC5 0846851D F9AB4819 5DED7EA1
B1D510BD 7EE74D73 FAF36BC3 1ECFA268 359046F4 EB879F92
4009438B 481C6CD7 889A002E D5EE382B C9190DA6 FC026E47
9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71
60C980DD 98EDD3DF FFFFFFFF FFFFFFFF'''
]
all_ascii = ''.join(map(chr, range(256)))
def hex_to_decimal(stripee):
if not stripee:
return None
return int(stripee.translate(all_ascii, string.whitespace), 16)
primes = map(hex_to_decimal, hex_primes)

View File

@ -54,8 +54,16 @@ class SessionBusNotPresent(Exception):
def __str__(self):
return _('Session bus is not available.\nTry reading http://trac.gajim.org/wiki/GajimDBus')
class NegotiationError(Exception):
'''A session negotiation failed'''
pass
class DecryptionError(Exception):
'''A message couldn't be decrypted into usable XML'''
pass
class GajimGeneralException(Exception):
'''This exception ir our general exception'''
'''This exception is our general exception'''
def __init__(self, text=''):
Exception.__init__(self)
self.text = text

View File

@ -0,0 +1,927 @@
from common import gajim
from common import xmpp
from common import helpers
from common import exceptions
import random
import string
import math
import os
import time
from common import dh
import xmpp.c14n
from Crypto.Cipher import AES
from Crypto.Hash import HMAC, SHA256
from Crypto.PublicKey import RSA
import base64
XmlDsig = 'http://www.w3.org/2000/09/xmldsig#'
class StanzaSession(object):
def __init__(self, conn, jid, thread_id, type):
self.conn = conn
self.jid = jid
self.type = type
if thread_id:
self.received_thread_id = True
self.thread_id = thread_id
else:
self.received_thread_id = False
if type == 'normal':
self.thread_id = None
else:
self.thread_id = self.generate_thread_id()
self.last_send = 0
self.status = None
self.negotiated = {}
def generate_thread_id(self):
return "".join([random.choice(string.letters) for x in xrange(0,32)])
def send(self, msg):
if self.thread_id:
msg.NT.thread = self.thread_id
msg.setAttr('to', self.jid)
self.conn.send_stanza(msg)
if isinstance(msg, xmpp.Message):
self.last_send = time.time()
def reject_negotiation(self, body = None):
msg = xmpp.Message()
feature = msg.NT.feature
feature.setNamespace(xmpp.NS_FEATURE)
x = xmpp.DataForm(typ='submit')
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
x.addChild(node=xmpp.DataField(name='accept', value='0'))
feature.addChild(node=x)
if body:
msg.setBody(body)
self.send(msg)
self.cancelled_negotiation()
def cancelled_negotiation(self):
'''A negotiation has been cancelled, so reset this session to its default state.'''
self.status = None
self.negotiated = {}
def terminate(self):
msg = xmpp.Message()
feature = msg.NT.feature
feature.setNamespace(xmpp.NS_FEATURE)
x = xmpp.DataForm(typ='submit')
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
x.addChild(node=xmpp.DataField(name='terminate', value='1'))
feature.addChild(node=x)
self.send(msg)
self.status = None
def acknowledge_termination(self):
# we could send an acknowledgement message here, but we won't.
self.status = None
# an encrypted stanza negotiation has several states. i've represented them as the following values in the 'status'
# attribute of the session object:
# 1. None:
# default state
# 2. 'requested-e2e':
# this client has initiated an esession negotiation and is waiting for
# a response
# 3. 'responded-e2e':
# this client has responded to an esession negotiation request and is
# waiting for the initiator to identify itself and complete the
# negotiation
# 4. 'identified-alice':
# this client identified itself and is waiting for the responder to
# identify itself and complete the negotiation
# 5. 'active':
# an encrypted session has been successfully negotiated. messages of
# any of the types listed in 'encryptable_stanzas' should be encrypted
# before they're sent.
# the transition between these states is handled in gajim.py's
# handle_session_negotiation method.
class EncryptedStanzaSession(StanzaSession):
def __init__(self, conn, jid, thread_id, type = 'chat'):
StanzaSession.__init__(self, conn, jid, thread_id, type = 'chat')
self.loggable = True
self.xes = {}
self.es = {}
self.n = 128
self.enable_encryption = False
# _s denotes 'self' (ie. this client)
self._kc_s = None
# _o denotes 'other' (ie. the client at the other end of the session)
self._kc_o = None
# keep the encrypter updated with my latest cipher key
def set_kc_s(self, value):
self._kc_s = value
self.encrypter = self.cipher.new(self._kc_s, self.cipher.MODE_CTR, counter=self.encryptcounter)
def get_kc_s(self):
return self._kc_s
# keep the decrypter updated with the other party's latest cipher key
def set_kc_o(self, value):
self._kc_o = value
self.decrypter = self.cipher.new(self._kc_o, self.cipher.MODE_CTR, counter=self.decryptcounter)
def get_kc_o(self):
return self._kc_o
kc_s = property(get_kc_s, set_kc_s)
kc_o = property(get_kc_o, set_kc_o)
# 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.c_s = (self.c_s + 1) % (2 ** self.n)
return self.encode_mpi_with_padding(self.c_s)
def decryptcounter(self):
self.c_o = (self.c_o + 1) % (2 ** self.n)
return self.encode_mpi_with_padding(self.c_o)
def sign(self, string):
if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'):
hash = self.sha256(string)
return self.encode_mpi(gajim.interface.pubkey.sign(hash, '')[0])
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.c_s
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(self.km_s, m_content + self.encode_mpi(old_en_counter)))
return stanza
def hmac(self, key, content):
return HMAC.new(key, content, self.hash_alg).digest()
def sha256(self, string):
sh = SHA256.new()
sh.update(string)
return sh.digest()
base28_chr = "acdefghikmopqruvwxy123456789"
def sas_28x5(self, m_a, form_b):
sha = self.sha256(m_a + form_b + 'Short Authentication String')
lsb24 = self.decode_mpi(sha[-3:])
return self.base28(lsb24)
def base28(self, n):
if n >= 28:
return self.base28(n / 28) + self.base28_chr[n % 28]
else:
return self.base28_chr[n]
def generate_initiator_keys(self, k):
return (self.hmac(k, 'Initiator Cipher Key'),
self.hmac(k, 'Initiator MAC Key'),
self.hmac(k, 'Initiator SIGMA Key') )
def generate_responder_keys(self, k):
return (self.hmac(k, 'Responder Cipher Key'),
self.hmac(k, 'Responder MAC Key'),
self.hmac(k, 'Responder SIGMA Key') )
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)
if len_padding != 16:
encryptable += len_padding * ' '
return self.encrypter.encrypt(encryptable)
# FIXME: use a real PRNG
def random_bytes(self, bytes):
return os.urandom(bytes)
def generate_nonce(self):
return self.random_bytes(8)
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(self.km_o, macable + self.encode_mpi_with_padding(self.c_o))
if not calculated_mac == received_mac:
raise exceptions.DecryptionError, 'bad signature'
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 exceptions.DecryptionError, 'decrypted <data/> not parseable as XML'
for child in parsed.getChildren():
stanza.addChild(node=child)
return stanza
def decrypt(self, ciphertext):
return self.decrypter.decrypt(ciphertext)
def logging_preference(self):
if gajim.config.get('log_encrypted_sessions'):
return ["may", "mustnot"]
else:
return ["mustnot", "may"]
def get_shared_secret(self, e, y, p):
if (not 1 < e < (p - 1)):
raise exceptions.NegotiationError, "invalid DH value"
return self.sha256(self.encode_mpi(self.powmod(e, y, p)))
def c7lize_mac_id(self, form):
kids = form.getChildren()
macable = filter(lambda x: x.getVar() not in ('mac', 'identity'), kids)
return ''.join(map(lambda el: xmpp.c14n.c14n(el), macable))
def verify_identity(self, form, dh_i, sigmai, i_o):
m_o = base64.b64decode(form['mac'])
id_o = base64.b64decode(form['identity'])
m_o_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_o)
if m_o_calculated != m_o:
raise exceptions.NegotiationError, 'calculated m_%s differs from received m_%s' % (i_o, i_o)
if i_o == 'a' and self.sas_algs == 'sas28x5':
# XXX not necessary if there's a verified retained secret
self.sas = self.sas_28x5(m_o, self.form_s)
if self.negotiated['recv_pubkey']:
plaintext = self.decrypt(id_o)
parsed = xmpp.Node(node='<node>' + plaintext + '</node>')
if self.negotiated['recv_pubkey'] == 'hash':
fingerprint = parsed.getTagData('fingerprint')
# XXX find stored pubkey or terminate session
raise 'unimplemented'
else:
if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'):
keyvalue = parsed.getTag(name='RSAKeyValue', namespace=XmlDsig)
n, e = map(lambda x: self.decode_mpi(base64.b64decode(keyvalue.getTagData(x))), ('Modulus', 'Exponent'))
eir_pubkey = RSA.construct((n,long(e)))
pubkey_o = xmpp.c14n.c14n(keyvalue)
else:
# XXX DSA, etc.
raise 'unimplemented'
enc_sig = parsed.getTag(name='SignatureValue', namespace=XmlDsig).getData()
signature = (self.decode_mpi(base64.b64decode(enc_sig)),)
else:
mac_o = self.decrypt(id_o)
pubkey_o = ''
c7l_form = self.c7lize_mac_id(form)
content = self.n_s + self.n_o + self.encode_mpi(dh_i) + pubkey_o
if sigmai:
self.form_o = c7l_form
content += self.form_o
else:
form_o2 = c7l_form
content += self.form_o + form_o2
mac_o_calculated = self.hmac(self.ks_o, content)
if self.negotiated['recv_pubkey']:
hash = self.sha256(mac_o_calculated)
if not eir_pubkey.verify(hash, signature):
raise exceptions.NegotiationError, 'public key signature verification failed!'
elif mac_o_calculated != mac_o:
raise exceptions.NegotiationError, 'calculated mac_%s differs from received mac_%s' % (i_o, i_o)
def make_identity(self, form, dh_i):
if self.negotiated['send_pubkey']:
if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'):
fields = (gajim.interface.pubkey.n, gajim.interface.pubkey.e)
cb_fields = map(lambda f: base64.b64encode(self.encode_mpi(f)), fields)
pubkey_s = '<RSAKeyValue xmlns="http://www.w3.org/2000/09/xmldsig#"><Modulus>%s</Modulus><Exponent>%s</Exponent></RSAKeyValue>' % tuple(cb_fields)
else:
pubkey_s = ''
form_s2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren()))
old_c_s = self.c_s
content = self.n_o + self.n_s + self.encode_mpi(dh_i) + pubkey_s + self.form_s + form_s2
mac_s = self.hmac(self.ks_s, content)
if self.negotiated['send_pubkey']:
signature = self.sign(mac_s)
sign_s = '<SignatureValue xmlns="http://www.w3.org/2000/09/xmldsig#">%s</SignatureValue>' % base64.b64encode(signature)
if self.negotiated['send_pubkey'] == 'hash':
b64ed = base64.b64encode(self.hash(pubkey_s))
pubkey_s = '<fingerprint>%s</fingerprint>' % b64ed
id_s = self.encrypt(pubkey_s + sign_s)
else:
id_s = self.encrypt(mac_s)
m_s = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_s)
if self.status == 'requested-e2e' and self.sas_algs == 'sas28x5':
# we're alice; check for a retained secret
# if none exists, prompt the user with the SAS
self.sas = self.sas_28x5(m_s, self.form_o)
if self.sigmai:
self.check_identity()
return (xmpp.DataField(name='identity', value=base64.b64encode(id_s)), \
xmpp.DataField(name='mac', value=base64.b64encode(m_s)))
def negotiate_e2e(self, sigmai):
self.negotiated = {}
request = xmpp.Message()
feature = request.NT.feature
feature.setNamespace(xmpp.NS_FEATURE)
x = xmpp.DataForm(typ='form')
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn', typ='hidden'))
x.addChild(node=xmpp.DataField(name='accept', value='1', typ='boolean', required=True))
# this field is incorrectly called 'otr' in XEPs 0116 and 0217
x.addChild(node=xmpp.DataField(name='logging', typ='list-single', options=self.logging_preference(), required=True))
# unsupported options: 'disabled', 'enabled'
x.addChild(node=xmpp.DataField(name='disclosure', typ='list-single', options=['never'], required=True))
x.addChild(node=xmpp.DataField(name='security', typ='list-single', options=['e2e'], required=True))
x.addChild(node=xmpp.DataField(name='crypt_algs', value='aes128-ctr', typ='hidden'))
x.addChild(node=xmpp.DataField(name='hash_algs', value='sha256', typ='hidden'))
x.addChild(node=xmpp.DataField(name='compress', value='none', typ='hidden'))
# unsupported options: 'iq', 'presence'
x.addChild(node=xmpp.DataField(name='stanzas', typ='list-multi', options=['message']))
x.addChild(node=xmpp.DataField(name='init_pubkey', options=['none', 'key', 'hash'], typ='list-single'))
# XXX store key, use hash
x.addChild(node=xmpp.DataField(name='resp_pubkey', options=['none', 'key'], typ='list-single'))
x.addChild(node=xmpp.DataField(name='ver', value='1.0', typ='hidden'))
x.addChild(node=xmpp.DataField(name='rekey_freq', value='4294967295', typ='hidden'))
x.addChild(node=xmpp.DataField(name='sas_algs', value='sas28x5', typ='hidden'))
x.addChild(node=xmpp.DataField(name='sign_algs', value='http://www.w3.org/2000/09/xmldsig#rsa-sha256', typ='hidden'))
self.n_s = self.generate_nonce()
x.addChild(node=xmpp.DataField(name='my_nonce', value=base64.b64encode(self.n_s), typ='hidden'))
modp_options = [ 5, 14, 2, 1 ]
x.addChild(node=xmpp.DataField(name='modp', typ='list-single', options=map(lambda x: [ None, x ], modp_options)))
x.addChild(node=self.make_dhfield(modp_options, sigmai))
self.sigmai = sigmai
self.form_s = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren()))
feature.addChild(node=x)
self.status = 'requested-e2e'
self.send(request)
# 4.3 esession response (bob)
def verify_options_bob(self, form):
negotiated = {'recv_pubkey': None, 'send_pubkey': None}
not_acceptable = []
ask_user = {}
fixed = { 'disclosure': 'never',
'security': 'e2e',
'crypt_algs': 'aes128-ctr',
'hash_algs': 'sha256',
'compress': 'none',
'stanzas': 'message',
'init_pubkey': 'none',
'resp_pubkey': 'none',
'ver': '1.0',
'sas_algs': 'sas28x5' }
self.encryptable_stanzas = ['message']
self.sas_algs = 'sas28x5'
self.cipher = AES
self.hash_alg = SHA256
self.compression = None
for name, field in map(lambda name: (name, form.getField(name)), form.asDict().keys()):
options = map(lambda x: x[1], field.getOptions())
values = field.getValues()
if not field.getType() in ('list-single', 'list-multi'):
options = values
if name in fixed:
if fixed[name] in options:
negotiated[name] = fixed[name]
else:
not_acceptable.append(name)
elif name == 'rekey_freq':
preferred = int(options[0])
negotiated['rekey_freq'] = preferred
self.rekey_freq = preferred
elif name == 'logging':
my_prefs = self.logging_preference()
if my_prefs[0] in options: # our first choice is offered, select it
pref = my_prefs[0]
negotiated['logging'] = pref
else: # see if other acceptable choices are offered
for pref in my_prefs:
if pref in options:
ask_user['logging'] = pref
break
if not 'logging' in ask_user:
not_acceptable.append(name)
elif name == 'init_pubkey':
for x in ('key'):
if x in options:
negotiated['recv_pubkey'] = x
break
elif name == 'resp_pubkey':
for x in ('hash', 'key'):
if x in options:
negotiated['send_pubkey'] = x
break
elif name == 'sign_algs':
if (XmlDsig + 'rsa-sha256') in options:
negotiated['sign_algs'] = XmlDsig + 'rsa-sha256'
else:
# XXX some things are handled elsewhere, some things are not-implemented
pass
return (negotiated, not_acceptable, ask_user)
# 4.3 esession response (bob)
def respond_e2e_bob(self, form, negotiated, not_acceptable):
response = xmpp.Message()
feature = response.NT.feature
feature.setNamespace(xmpp.NS_FEATURE)
x = xmpp.DataForm(typ='submit')
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
x.addChild(node=xmpp.DataField(name='accept', value='true'))
for name in negotiated:
# some fields are internal and should not be sent
if not name in ('send_pubkey', 'recv_pubkey'):
x.addChild(node=xmpp.DataField(name=name, value=negotiated[name]))
self.negotiated = negotiated
# the offset of the group we chose (need it to match up with the dhhash)
group_order = 0
self.modp = int(form.getField('modp').getOptions()[group_order][1])
x.addChild(node=xmpp.DataField(name='modp', value=self.modp))
g = dh.generators[self.modp]
p = dh.primes[self.modp]
self.n_o = base64.b64decode(form['my_nonce'])
dhhashes = form.getField('dhhashes').getValues()
self.negotiated['He'] = base64.b64decode(dhhashes[group_order].encode("utf8"))
bytes = int(self.n / 8)
self.n_s = self.generate_nonce()
self.c_o = self.decode_mpi(self.random_bytes(bytes)) # n-bit random number
self.c_s = self.c_o ^ (2 ** (self.n - 1))
self.y = self.srand(2 ** (2 * self.n - 1), p - 1)
self.d = self.powmod(g, self.y, p)
to_add = { 'my_nonce': self.n_s,
'dhkeys': self.encode_mpi(self.d),
'counter': self.encode_mpi(self.c_o),
'nonce': self.n_o }
for name in to_add:
b64ed = base64.b64encode(to_add[name])
x.addChild(node=xmpp.DataField(name=name, value=b64ed))
self.form_o = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren()))
self.form_s = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren()))
self.status = 'responded-e2e'
feature.addChild(node=x)
if not_acceptable:
response = xmpp.Error(response, xmpp.ERR_NOT_ACCEPTABLE)
feature = xmpp.Node(xmpp.NS_FEATURE + ' feature')
for f in not_acceptable:
n = xmpp.Node('field')
n['var'] = f
feature.addChild(node=n)
response.T.error.addChild(node=feature)
self.send(response)
# 'Alice Accepts'
def verify_options_alice(self, form):
negotiated = {}
ask_user = {}
not_acceptable = []
if not form['logging'] in self.logging_preference():
not_acceptable.append(form['logging'])
elif form['logging'] != self.logging_preference()[0]:
ask_user['logging'] = form['logging']
else:
negotiated['logging'] = self.logging_preference()[0]
for r,a in (('recv_pubkey', 'resp_pubkey'), ('send_pubkey', 'init_pubkey')):
negotiated[r] = None
if a in form.asDict() and form[a] in ('key', 'hash'):
negotiated[r] = form[a]
if 'sign_algs' in form.asDict():
if form['sign_algs'] in (XmlDsig + 'rsa-sha256',):
negotiated['sign_algs'] = form['sign_algs']
else:
not_acceptable.append(form['sign_algs'])
return (negotiated, not_acceptable, ask_user)
# 'Alice Accepts', continued
def accept_e2e_alice(self, form, negotiated):
self.encryptable_stanzas = ['message']
self.sas_algs = 'sas28x5'
self.cipher = AES
self.hash_alg = SHA256
self.compression = None
self.negotiated = negotiated
accept = xmpp.Message()
feature = accept.NT.feature
feature.setNamespace(xmpp.NS_FEATURE)
result = xmpp.DataForm(typ='result')
self.c_s = self.decode_mpi(base64.b64decode(form['counter']))
self.c_o = self.c_s ^ (2 ** (self.n - 1))
self.n_o = base64.b64decode(form['my_nonce'])
mod_p = int(form['modp'])
p = dh.primes[mod_p]
x = self.xes[mod_p]
e = self.es[mod_p]
self.d = self.decode_mpi(base64.b64decode(form['dhkeys']))
self.k = self.get_shared_secret(self.d, x, p)
result.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
result.addChild(node=xmpp.DataField(name='accept', value='1'))
result.addChild(node=xmpp.DataField(name='nonce', value=base64.b64encode(self.n_o)))
self.kc_s, self.km_s, self.ks_s = self.generate_initiator_keys(self.k)
if self.sigmai:
self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(self.k)
self.verify_identity(form, self.d, True, 'b')
else:
secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped())
rshashes = [self.hmac(self.n_s, rs) for rs in secrets]
# XXX add some random fake rshashes here
rshashes.sort()
rshashes = [base64.b64encode(rshash) for rshash in rshashes]
result.addChild(node=xmpp.DataField(name='rshashes', value=rshashes))
result.addChild(node=xmpp.DataField(name='dhkeys', value=base64.b64encode(self.encode_mpi(e))))
self.form_o = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren()))
# MUST securely destroy K unless it will be used later to generate the final shared secret
for datafield in self.make_identity(result, e):
result.addChild(node=datafield)
feature.addChild(node=result)
self.send(accept)
if self.sigmai:
self.status = 'active'
self.enable_encryption = True
else:
self.status = 'identified-alice'
# 4.5 esession accept (bob)
def accept_e2e_bob(self, form):
response = xmpp.Message()
init = response.NT.init
init.setNamespace(xmpp.NS_ESESSION_INIT)
x = xmpp.DataForm(typ='result')
for field in ('nonce', 'dhkeys', 'rshashes', 'identity', 'mac'):
assert field in form.asDict(), "alice's form didn't have a %s field" % field
# 4.5.1 generating provisory session keys
e = self.decode_mpi(base64.b64decode(form['dhkeys']))
p = dh.primes[self.modp]
if self.sha256(self.encode_mpi(e)) != self.negotiated['He']:
raise exceptions.NegotiationError, 'SHA256(e) != He'
k = self.get_shared_secret(e, self.y, p)
self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k)
# 4.5.2 verifying alice's identity
self.verify_identity(form, e, False, 'a')
# 4.5.4 generating bob's final session keys
srs = ''
secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped())
rshashes = [base64.b64decode(rshash) for rshash in form.getField('rshashes').getValues()]
for secret in secrets:
if self.hmac(self.n_o, secret) in rshashes:
srs = secret
break
# other shared secret, we haven't got one.
oss = ''
k = self.sha256(k + srs + oss)
self.kc_s, self.km_s, self.ks_s = self.generate_responder_keys(k)
self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k)
# 4.5.5
if srs:
srshash = self.hmac(srs, 'Shared Retained Secret')
else:
srshash = self.random_bytes(32)
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
x.addChild(node=xmpp.DataField(name='nonce', value=base64.b64encode(self.n_o)))
x.addChild(node=xmpp.DataField(name='srshash', value=base64.b64encode(srshash)))
for datafield in self.make_identity(x, self.d):
x.addChild(node=datafield)
init.addChild(node=x)
self.send(response)
self.do_retained_secret(k, srs)
if self.negotiated['logging'] == 'mustnot':
self.loggable = False
self.status = 'active'
self.enable_encryption = True
def final_steps_alice(self, form):
srs = ''
secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped())
srshash = base64.b64decode(form['srshash'])
for secret in secrets:
if self.hmac(secret, 'Shared Retained Secret') == srshash:
srs = secret
break
oss = ''
k = self.sha256(self.k + srs + oss)
del self.k
self.do_retained_secret(k, srs)
# don't need to calculate ks_s here
self.kc_s, self.km_s, self.ks_s = self.generate_initiator_keys(k)
self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(k)
# 4.6.2 Verifying Bob's Identity
self.verify_identity(form, self.d, False, 'b')
# Note: If Alice discovers an error then she SHOULD ignore any encrypted content she received in the stanza.
if self.negotiated['logging'] == 'mustnot':
self.loggable = False
self.status = 'active'
self.enable_encryption = True
# calculate and store the new retained secret
# prompt the user to check the remote party's identity (if necessary)
def do_retained_secret(self, k, srs):
new_srs = self.hmac(k, 'New Retained Secret')
account = self.conn.name
bjid = self.jid.getStripped()
if srs:
gajim.interface.replace_secret(account, bjid, srs, new_srs)
else:
self.check_identity()
gajim.interface.save_new_secret(account, bjid, new_srs)
# generate a random number between 'bottom' and 'top'
def srand(self, bottom, top):
# minimum number of bytes needed to represent that range
bytes = int(math.ceil(math.log(top - bottom, 256)))
# in retrospect, this is horribly inadequate.
return (self.decode_mpi(self.random_bytes(bytes)) % (top - bottom)) + bottom
def make_dhfield(self, modp_options, sigmai):
dhs = []
for modp in modp_options:
p = dh.primes[modp]
g = dh.generators[modp]
x = self.srand(2 ** (2 * self.n - 1), p - 1)
# XXX this may be a source of performance issues
e = self.powmod(g, x, p)
self.xes[modp] = x
self.es[modp] = e
if sigmai:
dhs.append(base64.b64encode(self.encode_mpi(e)))
name = 'dhkeys'
else:
He = self.sha256(self.encode_mpi(e))
dhs.append(base64.b64encode(He))
name = 'dhhashes'
return xmpp.DataField(name=name, typ='hidden', value=dhs)
# a faster version of (base ** exp) % mod
# taken from <http://lists.danga.com/pipermail/yadis/2005-September/001445.html>
def powmod(self, base, exp, mod):
square = base % mod
result = 1
while exp > 0:
if exp & 1: # exponent is odd
result = (result * square) % mod
square = (square * square) % mod
exp /= 2
return result
def terminate_e2e(self):
self.terminate()
self.enable_encryption = False
def acknowledge_termination(self):
StanzaSession.acknowledge_termination(self)
self.enable_encryption = False
def fail_bad_negotiation(self, reason):
'''they've tried to feed us a bogus value, send an error and cancel everything.'''
err = xmpp.Error(xmpp.Message(), xmpp.ERR_FEATURE_NOT_IMPLEMENTED)
err.T.error.T.text.setData(reason)
self.send(err)
self.status = None
self.enable_encryption = False
# this prevents the MAC check on decryption from succeeding,
# preventing falsified messages from going through.
self.km_o = ''
def is_loggable(self):
account = self.conn.name
no_log_for = gajim.config.get_per('accounts', account, 'no_log_for')
if not no_log_for:
no_log_for = ''
no_log_for = no_log_for.split()
return self.loggable and account not in no_log_for and self.jid not in no_log_for

36
src/common/xmpp/c14n.py Normal file
View File

@ -0,0 +1,36 @@
from simplexml import ustr
# XML canonicalisation methods (for XEP-0116)
def c14n(node):
s = "<" + node.name
if node.namespace:
if not node.parent or node.parent.namespace != node.namespace:
s = s + ' xmlns="%s"' % node.namespace
sorted_attrs = node.attrs.keys()
sorted_attrs.sort()
for key in sorted_attrs:
val = ustr(node.attrs[key])
# like XMLescape() but with whitespace and without &gt;
s = s + ' %s="%s"' % ( key, normalise_attr(val) )
s = s + ">"
cnt = 0
if node.kids:
for a in node.kids:
if (len(node.data)-1) >= cnt:
s = s + normalise_text(node.data[cnt])
s = s + c14n(a)
cnt=cnt+1
if (len(node.data)-1) >= cnt: s = s + normalise_text(node.data[cnt])
if not node.kids and s[-1:]=='>':
s=s[:-1]+' />'
else:
s = s + "</" + node.name + ">"
return s.encode('utf-8')
def normalise_attr(val):
return val.replace('&', '&amp;').replace('<', '&lt;').replace('"', '&quot;').replace('\t', '&#x9;').replace('\n', '&#xA;').replace('\r', '&#xD;')
def normalise_text(val):
return val.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('\r', '&#xD;')

View File

@ -45,6 +45,7 @@ NS_DISCO ='http://jabber.org/protocol/disco'
NS_DISCO_INFO =NS_DISCO+'#info'
NS_DISCO_ITEMS =NS_DISCO+'#items'
NS_ENCRYPTED ='jabber:x:encrypted' # XEP-0027
NS_ESESSION_INIT='http://www.xmpp.org/extensions/xep-0116.html#ns-init' # XEP-0116
NS_EVENT ='jabber:x:event' # XEP-0022
NS_FEATURE ='http://jabber.org/protocol/feature-neg'
NS_FILE ='http://jabber.org/protocol/si/profile/file-transfer' # XEP-0096
@ -82,6 +83,7 @@ NS_SESSION ='urn:ietf:params:xml:ns:xmpp-session'
NS_SI ='http://jabber.org/protocol/si' # XEP-0096
NS_SI_PUB ='http://jabber.org/protocol/sipub' # XEP-0137
NS_SIGNED ='jabber:x:signed' # XEP-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'

View File

@ -639,6 +639,7 @@ class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream):
msghtml = msg.getXHTML()
mtype = msg.getType()
subject = msg.getSubject() # if not there, it's None
thread = msg.getThread()
tim = msg.getTimestamp()
tim = helpers.datetime_tuple(tim)
tim = time.localtime(timegm(tim))
@ -715,7 +716,7 @@ class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream):
msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim,
subject = subject)
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject,
chatstate, msg_id, composing_xep, user_nick, msghtml))
chatstate, msg_id, composing_jep, user_nick, msghtml, thread))
elif mtype == 'normal': # it's single message
if self.name not in no_log_for and jid not in no_log_for and msgtxt:
gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim,

View File

@ -199,7 +199,7 @@ class EditGroupsDialog:
for group in groups:
if group not in helpers.special_groups or groups[group] > 0:
group_list.append(group)
group_list.sort()
group_list.sort()
for group in group_list:
iter = store.append()
store.set(iter, 0, group) # Group name
@ -1427,7 +1427,7 @@ class SynchroniseSelectAccountDialog:
if not iter:
return
remote_account = model.get_value(iter, 0).decode('utf-8')
if gajim.connections[remote_account].connected < 2:
ErrorDialog(_('This account is not connected to the server'),
_('You cannot synchronize with an account unless it is connected.'))
@ -1709,7 +1709,7 @@ class SingleMessageWindow:
or 'receive'.
'''
def __init__(self, account, to = '', action = '', from_whom = '',
subject = '', message = '', resource = ''):
subject = '', message = '', resource = '', session = None):
self.account = account
self.action = action
@ -1718,6 +1718,7 @@ class SingleMessageWindow:
self.to = to
self.from_whom = from_whom
self.resource = resource
self.session = session
self.xml = gtkgui_helpers.get_glade('single_message_window.glade')
self.window = self.xml.get_widget('single_message_window')
@ -1908,7 +1909,7 @@ class SingleMessageWindow:
# FIXME: allow GPG message some day
gajim.connections[self.account].send_message(to_whom_jid, message,
keyID = None, type = 'normal', subject=subject)
keyID = None, type = 'normal', subject=subject, session = self.session)
self.subject_entry.set_text('') # we sent ok, clear the subject
self.message_tv_buffer.set_text('') # we sent ok, clear the textview
@ -1925,7 +1926,7 @@ class SingleMessageWindow:
self.window.destroy()
SingleMessageWindow(self.account, to = self.from_whom,
action = 'send', from_whom = self.from_whom, subject = self.subject,
message = self.message)
message = self.message, session = self.session)
def on_send_and_close_button_clicked(self, widget):
self.send_single_message()
@ -2111,7 +2112,6 @@ class PrivacyListWindow:
jid_entry_completion.set_model(jids_list_store)
jid_entry_completion.set_popup_completion(True)
self.edit_type_jabberid_entry.set_completion(jid_entry_completion)
if action == 'EDIT':
self.refresh_rules()

View File

@ -117,12 +117,17 @@ import message_control
from chat_control import ChatControlBase
from atom_window import AtomWindow
import negotiation
import Crypto.PublicKey.RSA
from common import exceptions
from common.zeroconf import connection_zeroconf
from common import dbus_support
if dbus_support.supported:
import dbus
import pickle
if os.name == 'posix': # dl module is Unix Only
try: # rename the process name to gajim
import dl
@ -217,6 +222,7 @@ gajimpaths = common.configpaths.gajimpaths
pid_filename = gajimpaths['PID_FILE']
config_filename = gajimpaths['CONFIG_FILE']
secrets_filename = gajimpaths['SECRETS_FILE']
import traceback
import errno
@ -686,11 +692,11 @@ class Interface:
# It's maybe a GC_NOTIFY (specialy for MSN gc)
self.handle_event_gc_notify(account, (jid, array[1], status_message,
array[3], None, None, None, None, None, None, None, None))
def handle_event_msg(self, account, array):
# 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject,
# chatstate, msg_id, composing_xep, user_nick, xhtml))
# chatstate, msg_id, composing_xep, user_nick, xhtml, session))
# user_nick is JEP-0172
full_jid_with_resource = array[0]
@ -705,6 +711,7 @@ class Interface:
msg_id = array[7]
composing_xep = array[8]
xhtml = array[10]
session = array[11]
if gajim.config.get('ignore_incoming_xhtml'):
xhtml = None
if gajim.jid_is_transport(jid):
@ -791,20 +798,20 @@ class Interface:
if pm:
nickname = resource
groupchat_control.on_private_message(nickname, message, array[2],
xhtml, msg_id)
xhtml, session, msg_id)
else:
# array: (jid, msg, time, encrypted, msg_type, subject)
if encrypted:
self.roster.on_message(jid, message, array[2], account, array[3],
msg_type, subject, resource, msg_id, array[9],
advanced_notif_num)
advanced_notif_num, session = session)
else:
# xhtml in last element
self.roster.on_message(jid, message, array[2], account, array[3],
msg_type, subject, resource, msg_id, array[9],
advanced_notif_num, xhtml = xhtml)
advanced_notif_num, xhtml = xhtml, session = session)
nickname = gajim.get_name_from_jid(account, jid)
# Check and do wanted notifications
# Check and do wanted notifications
msg = message
if subject:
msg = _('Subject: %s') % subject + '\n' + msg
@ -1501,6 +1508,48 @@ class Interface:
if os.path.isfile(path_to_original_file):
os.remove(path_to_original_file)
# list the retained secrets we have for a local account and a remote jid
def list_secrets(self, account, jid):
f = open(secrets_filename)
try:
s = pickle.load(f)[account][jid]
except KeyError:
s = []
f.close()
return s
# save a new retained secret
def save_new_secret(self, account, jid, secret):
f = open(secrets_filename, 'r')
secrets = pickle.load(f)
f.close()
if not account in secrets:
secrets[account] = {}
if not jid in secrets[account]:
secrets[account][jid] = []
secrets[account][jid].append(secret)
f = open(secrets_filename, 'w')
pickle.dump(secrets, f)
f.close()
def replace_secret(self, account, jid, old_secret, new_secret):
f = open(secrets_filename, 'r')
secrets = pickle.load(f)
f.close()
this_secrets = secrets[account][jid]
this_secrets[this_secrets.index(old_secret)] = new_secret
f = open(secrets_filename, 'w')
pickle.dump(secrets, f)
f.close()
def add_event(self, account, jid, type_, event_args):
'''add an event to the gajim.events var'''
# We add it to the gajim.events queue
@ -1768,6 +1817,162 @@ class Interface:
atom_entry, = data
AtomWindow.newAtomEntry(atom_entry)
def handle_event_failed_decrypt(self, account, data):
jid, tim = data
ctrl = self.msg_win_mgr.get_control(jid, account)
if ctrl:
ctrl.print_conversation_line('Unable to decrypt message from %s\nIt may have been tampered with.' % (jid), 'status', '', tim)
else:
print 'failed decrypt, unable to find a control to notify you in.'
def handle_session_negotiation(self, account, data):
jid, session, form = data
if form.getField('accept') and not form['accept'] in ('1', 'true'):
dialogs.InformationDialog(_('Session negotiation cancelled'),
_('The client at %s cancelled the session negotiation.') % (jid))
session.cancelled_negotiation()
return
# encrypted session states. these are described in stanza_session.py
# bob responds
if form.getType() == 'form' and 'security' in form.asDict():
def continue_with_negotiation(*args):
if len(args):
self.dialog.destroy()
# we don't support 3-message negotiation as the responder
if 'dhkeys' in form.asDict():
err = xmpp.Error(xmpp.Message(), xmpp.ERR_FEATURE_NOT_IMPLEMENTED)
feature = xmpp.Node(xmpp.NS_FEATURE + ' feature')
field = xmpp.Node('field')
field['var'] = 'dhkeys'
feature.addChild(node=field)
err.addChild(node=feature)
session.send(err)
return
negotiated, not_acceptable, ask_user = session.verify_options_bob(form)
if ask_user:
def accept_nondefault_options(widget):
self.dialog.destroy()
negotiated.update(ask_user)
session.respond_e2e_bob(form, negotiated, not_acceptable)
def reject_nondefault_options(widget):
self.dialog.destroy()
for key in ask_user.keys():
not_acceptable.append(key)
session.respond_e2e_bob(form, negotiated, not_acceptable)
self.dialog = dialogs.YesNoDialog(_('Confirm these session options'),
_('''The remote client wants to negotiate an session with these features:
%s
Are these options acceptable?''') % (negotiation.describe_features(ask_user)),
on_response_yes = accept_nondefault_options,
on_response_no = reject_nondefault_options)
else:
session.respond_e2e_bob(form, negotiated, not_acceptable)
def ignore_negotiation(widget):
self.dialog.destroy()
return
continue_with_negotiation()
return
# alice accepts
elif session.status == 'requested-e2e' and form.getType() == 'submit':
negotiated, not_acceptable, ask_user = session.verify_options_alice(form)
if session.sigmai:
session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas)
if ask_user:
def accept_nondefault_options(widget):
dialog.destroy()
negotiated.update(ask_user)
try:
session.accept_e2e_alice(form, negotiated)
except exceptions.NegotiationError, details:
session.fail_bad_negotiation(details)
def reject_nondefault_options(widget):
session.reject_negotiation()
dialog.destroy()
dialog = dialogs.YesNoDialog(_('Confirm these session options'),
_('The remote client selected these options:\n\n%s\n\nContinue with the session?') % (negotiation.describe_features(ask_user)),
on_response_yes = accept_nondefault_options,
on_response_no = reject_nondefault_options)
else:
try:
session.accept_e2e_alice(form, negotiated)
except exceptions.NegotiationError, details:
session.fail_bad_negotiation(details)
return
elif session.status == 'responded-e2e' and form.getType() == 'result':
session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas)
try:
session.accept_e2e_bob(form)
except exceptions.NegotiationError, details:
session.fail_bad_negotiation(details)
return
elif session.status == 'identified-alice' and form.getType() == 'result':
session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas)
try:
session.final_steps_alice(form)
except exceptions.NegotiationError, details:
session.fail_bad_negotiation(details)
return
if form.getField('terminate'):
if form.getField('terminate').getValue() in ('1', 'true'):
session.acknowledge_termination()
gajim.connections[account].delete_session(str(jid), session.thread_id)
ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account)
if ctrl:
ctrl.session = gajim.connections[account].make_new_session(str(jid))
return
# non-esession negotiation. this isn't very useful, but i'm keeping it around
# to test my test suite.
if form.getType() == 'form':
ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account)
if not ctrl:
resource = jid.getResource()
contact = gajim.contacts.get_contact(account, str(jid), resource)
if not contact:
connection = gajim.connections[account]
contact = gajim.contacts.create_contact(jid = jid.getStripped(), resource = resource, show = connection.get_status())
self.roster.new_chat(contact, account, resource = resource)
ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account)
ctrl.set_session(session)
negotiation.FeatureNegotiationWindow(account, jid, session, form)
def handle_event_privacy_lists_received(self, account, data):
# ('PRIVACY_LISTS_RECEIVED', account, list)
if not self.instances.has_key(account):
@ -2208,6 +2413,7 @@ class Interface:
'SIGNED_IN': self.handle_event_signed_in,
'METACONTACTS': self.handle_event_metacontacts,
'ATOM_ENTRY': self.handle_atom_entry,
'FAILED_DECRYPT': self.handle_event_failed_decrypt,
'PRIVACY_LISTS_RECEIVED': self.handle_event_privacy_lists_received,
'PRIVACY_LIST_RECEIVED': self.handle_event_privacy_list_received,
'PRIVACY_LISTS_ACTIVE_DEFAULT': \
@ -2223,6 +2429,7 @@ class Interface:
'UNIQUE_ROOM_ID_UNSUPPORTED': \
self.handle_event_unique_room_id_unsupported,
'UNIQUE_ROOM_ID_SUPPORTED': self.handle_event_unique_room_id_supported,
'SESSION_NEG': self.handle_session_negotiation,
}
gajim.handlers = self.handlers
@ -2545,6 +2752,10 @@ class Interface:
gobject.timeout_add(2000, self.process_connections)
gobject.timeout_add(10000, self.read_sleepy)
# public key for XEP-0116
# XXX os.urandom is not a cryptographic PRNG
self.pubkey = Crypto.PublicKey.RSA.generate(384, os.urandom)
if __name__ == '__main__':
def sigint_cb(num, stack):
sys.exit(5)
@ -2583,5 +2794,11 @@ if __name__ == '__main__':
check_paths.check_and_possibly_create_paths()
# create secrets file (unless it exists)
if not os.path.exists(secrets_filename):
f = open(secrets_filename, 'w')
pickle.dump({}, f)
f.close()
Interface()
gtk.main()

View File

@ -105,14 +105,14 @@ def tree_cell_data_func(column, renderer, model, iter, tv=None):
class PrivateChatControl(ChatControl):
TYPE_ID = message_control.TYPE_PM
def __init__(self, parent_win, gc_contact, contact, account):
def __init__(self, parent_win, gc_contact, contact, account, session):
room_jid = contact.jid.split('/')[0]
room_ctrl = gajim.interface.msg_win_mgr.get_control(room_jid, account)
if gajim.interface.minimized_controls[account].has_key(room_jid):
room_ctrl = gajim.interface.minimized_controls[account][room_jid]
self.room_name = room_ctrl.name
self.gc_contact = gc_contact
ChatControl.__init__(self, parent_win, contact, account)
ChatControl.__init__(self, parent_win, contact, account, session)
self.TYPE_ID = 'pm'
def send_message(self, message):
@ -544,7 +544,7 @@ class GroupchatControl(ChatControlBase):
else:
self.print_conversation(msg, nick, tim, xhtml)
def on_private_message(self, nick, msg, tim, xhtml, msg_id = None):
def on_private_message(self, nick, msg, tim, xhtml, session, msg_id = None):
# Do we have a queue?
fjid = self.room_jid + '/' + nick
no_queue = len(gajim.events.get_events(self.account, fjid)) == 0
@ -556,7 +556,7 @@ class GroupchatControl(ChatControlBase):
return
event = gajim.events.create_event('pm', (msg, '', 'incoming', tim,
False, '', msg_id, xhtml))
False, '', msg_id, xhtml, session))
gajim.events.add_event(self.account, fjid, event)
autopopup = gajim.config.get('autopopup')
@ -576,7 +576,7 @@ class GroupchatControl(ChatControlBase):
self.parent_win.show_title()
self.parent_win.redraw_tab(self)
else:
self._start_private_message(nick)
self._start_private_message(nick, session)
# Scroll to line
self.list_treeview.expand_row(path[0:1], False)
self.list_treeview.scroll_to_cell(path)
@ -1942,7 +1942,7 @@ class GroupchatControl(ChatControlBase):
menu.show_all()
menu.popup(None, None, None, event.button, event.time)
def _start_private_message(self, nick):
def _start_private_message(self, nick, session = None):
gc_c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
nick_jid = gc_c.get_full_jid()

View File

@ -110,14 +110,28 @@ class MessageControl:
def get_specific_unread(self):
return len(gajim.events.get_events(self.account, self.contact.jid))
def set_session(self, session):
if session == self.session:
return
if self.session:
print "starting a new session, dropping the old one!"
gajim.connections[self.account].delete_session(self.contact.get_full_jid(), self.session.thread_id)
self.session = session
def send_message(self, message, keyID = '', type = 'chat',
chatstate = None, msg_id = None, composing_xep = None, resource = None,
user_nick = None):
'''Send the given message to the active tab. Doesn't return None if error
'''
jid = self.contact.jid
if not self.session:
self.session = gajim.connections[self.account].make_new_session(self.contact.get_full_jid())
# Send and update history
return gajim.connections[self.account].send_message(jid, message, keyID,
type = type, chatstate = chatstate, msg_id = msg_id,
composing_xep = composing_xep, resource = self.resource,
user_nick = user_nick)
user_nick = user_nick, session = self.session)

75
src/negotiation.py Normal file
View File

@ -0,0 +1,75 @@
import gtkgui_helpers
import dataforms_widget
import dialogs
from common import dataforms
from common import gajim
from common import xmpp
def describe_features(features):
'''a human-readable description of the features that have been negotiated'''
if features['logging'] == 'may':
return _('- messages will be logged')
elif features['logging'] == 'mustnot':
return _('- messages will not be logged')
def show_sas_dialog(jid, sas):
dialogs.InformationDialog(_('''Verify the remote client's identity'''), _('''You've begun an encrypted session with %s, but it can't be guaranteed that you're talking directly to the person you think you are.
You should speak with them directly (in person or on the phone) and confirm that their Short Authentication String is identical to this one: %s''') % (jid, sas))
class FeatureNegotiationWindow:
'''FeatureNegotiotionWindow class'''
def __init__(self, account, jid, session, form):
self.account = account
self.jid = jid
self.form = form
self.session = session
self.xml = gtkgui_helpers.get_glade('data_form_window.glade', 'data_form_window')
self.window = self.xml.get_widget('data_form_window')
config_vbox = self.xml.get_widget('config_vbox')
dataform = dataforms.ExtendForm(node = self.form)
self.data_form_widget = dataforms_widget.DataFormWidget(dataform)
self.data_form_widget.show()
config_vbox.pack_start(self.data_form_widget)
self.xml.signal_autoconnect(self)
self.window.show_all()
def on_ok_button_clicked(self, widget):
acceptance = xmpp.Message(self.jid)
acceptance.setThread(self.session.thread_id)
feature = acceptance.NT.feature
feature.setNamespace(xmpp.NS_FEATURE)
form = self.data_form_widget.data_form
form.setAttr('type', 'submit')
feature.addChild(node=form)
gajim.connections[self.account].send_stanza(acceptance)
self.window.destroy()
def on_cancel_button_clicked(self, widget):
# XXX determine whether to reveal presence
rejection = xmpp.Message(self.jid)
rejection.setThread(self.session.thread_id)
feature = rejection.NT.feature
feature.setNamespace(xmpp.NS_FEATURE)
x = xmpp.DataForm(typ='submit')
x.addChild(node=xmpp.DataField('FORM_TYPE', value='urn:xmpp:ssn'))
x.addChild(node=xmpp.DataField('accept', value='false', typ='boolean'))
feature.addChild(node=x)
# XXX optional <body/>
gajim.connections[self.account].send_stanza(rejection)
self.window.destroy()

View File

@ -1230,10 +1230,15 @@ class RosterWindow:
'''reads from db the unread messages, and fire them up'''
for jid in gajim.contacts.get_jid_list(account):
results = gajim.logger.get_unread_msgs_for_jid(jid)
# XXX unread messages should probably have their session saved with them
if results:
session = gajim.connections[account].make_new_session(jid)
for result in results:
tim = time.localtime(float(result[2]))
self.on_message(jid, result[1], tim, account, msg_type = 'chat',
msg_id = result[0])
msg_id = result[0], session = session)
def fill_contacts_and_groups_dicts(self, array, account):
'''fill gajim.contacts and gajim.groups'''
@ -3758,7 +3763,7 @@ class RosterWindow:
self.actions_menu_needs_rebuild = True
self.update_status_combobox()
def new_private_chat(self, gc_contact, account):
def new_private_chat(self, gc_contact, account, session = None):
contact = gajim.contacts.contact_from_gc_contact(gc_contact)
type_ = message_control.TYPE_PM
fjid = gc_contact.room_jid + '/' + gc_contact.name
@ -3766,24 +3771,25 @@ class RosterWindow:
if not mw:
mw = gajim.interface.msg_win_mgr.create_window(contact, account, type_)
chat_control = PrivateChatControl(mw, gc_contact, contact, account)
chat_control = PrivateChatControl(mw, gc_contact, contact, account, session)
mw.new_tab(chat_control)
if len(gajim.events.get_events(account, fjid)):
# We call this here to avoid race conditions with widget validation
chat_control.read_queue()
def new_chat(self, contact, account, resource = None):
def new_chat(self, contact, account, resource = None, session = None):
# Get target window, create a control, and associate it with the window
type_ = message_control.TYPE_CHAT
fjid = contact.jid
if resource:
fjid += '/' + resource
mw = gajim.interface.msg_win_mgr.get_window(fjid, account)
if not mw:
mw = gajim.interface.msg_win_mgr.create_window(contact, account, type_)
chat_control = ChatControl(mw, contact, account, resource)
chat_control = ChatControl(mw, contact, account, session, resource)
mw.new_tab(chat_control)
@ -3827,7 +3833,7 @@ class RosterWindow:
def on_message(self, jid, msg, tim, account, encrypted = False,
msg_type = '', subject = None, resource = '', msg_id = None,
user_nick = '', advanced_notif_num = None, xhtml = None):
user_nick = '', advanced_notif_num = None, xhtml = None, session = None):
'''when we receive a message'''
contact = None
# if chat window will be for specific resource
@ -3884,7 +3890,7 @@ class RosterWindow:
if msg_type == 'normal' and popup: # it's single message to be autopopuped
dialogs.SingleMessageWindow(account, contact.jid,
action = 'receive', from_whom = jid, subject = subject,
message = msg, resource = resource)
message = msg, resource = resource, session = session)
return
# We print if window is opened and it's not a single message
@ -3892,6 +3898,8 @@ class RosterWindow:
typ = ''
if msg_type == 'error':
typ = 'status'
if session:
ctrl.set_session(session)
ctrl.print_conversation(msg, typ, tim = tim, encrypted = encrypted,
subject = subject, xhtml = xhtml)
if msg_id:
@ -3907,7 +3915,7 @@ class RosterWindow:
show_in_roster = notify.get_show_in_roster(event_type, account, contact)
show_in_systray = notify.get_show_in_systray(event_type, account, contact)
event = gajim.events.create_event(type_, (msg, subject, msg_type, tim,
encrypted, resource, msg_id, xhtml), show_in_roster = show_in_roster,
encrypted, resource, msg_id, xhtml, session), show_in_roster = show_in_roster,
show_in_systray = show_in_systray)
gajim.events.add_event(account, fjid, event)
if popup:
@ -4171,7 +4179,7 @@ class RosterWindow:
if event.type_ == 'normal':
dialogs.SingleMessageWindow(account, jid,
action = 'receive', from_whom = jid, subject = data[1],
message = data[0], resource = data[5])
message = data[0], resource = data[5], session = data[8])
gajim.interface.remove_first_event(account, jid, event.type_)
return True
elif event.type_ == 'file-request':
@ -4208,14 +4216,14 @@ class RosterWindow:
jid = jid + u'/' + resource
adhoc_commands.CommandWindow(account, jid)
def on_open_chat_window(self, widget, contact, account, resource = None):
def on_open_chat_window(self, widget, contact, account, resource = None, session = None):
# Get the window containing the chat
fjid = contact.jid
if resource:
fjid += '/' + resource
win = gajim.interface.msg_win_mgr.get_window(fjid, account)
if not win:
self.new_chat(contact, account, resource = resource)
self.new_chat(contact, account, resource = resource, session = session)
win = gajim.interface.msg_win_mgr.get_window(fjid, account)
ctrl = win.get_control(fjid, account)
# last message is long time ago
@ -4262,7 +4270,9 @@ class RosterWindow:
jid = child_jid
else:
child_iter = model.iter_next(child_iter)
session = None
if first_ev:
session = first_ev.parameters[8]
fjid = jid
if resource:
fjid += '/' + resource
@ -4273,7 +4283,7 @@ class RosterWindow:
c = gajim.contacts.get_contact_with_highest_priority(account, jid)
if jid == gajim.get_jid_from_account(account):
resource = c.resource
self.on_open_chat_window(widget, c, account, resource = resource)
self.on_open_chat_window(widget, c, account, resource = resource, session = session)
def on_roster_treeview_row_activated(self, widget, path, col = 0):
'''When an iter is double clicked: open the first event window'''