Philipp Hörist pushed to branch master at gajim / gajim
Commits: 32b74b45 by Philipp Hörist at 2018-05-28T18:21:45+02:00 Remove support for ESessions
Fixes #5294
- - - - -
23 changed files:
- README.md - gajim/chat_control.py - gajim/chat_control_base.py - gajim/common/app.py - gajim/common/config.py - gajim/common/connection.py - gajim/common/connection_handlers.py - gajim/common/connection_handlers_events.py - − gajim/common/crypto.py - gajim/common/exceptions.py - gajim/common/helpers.py - − gajim/common/stanza_session.py - gajim/features_window.py - gajim/groupchat_control.py - gajim/gui_interface.py - gajim/message_control.py - − gajim/secrets.py - gajim/session.py - org.gajim.Gajim.json - test/test_pluginmanager.py - test/unit/test_sessions.py - win/_base.sh - win/dev_env.sh
Changes:
===================================== README.md ===================================== --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@
- python3-keyring for saving your password to your system keyring - python3-pil (pillow) for support of webp avatars -- python3-crypto to enable End to end encryption - python3-gnupg to enable GPG encryption - For zeroconf (bonjour) you need python3-dbus - gir1.2-gspell-1 and hunspell-LANG where lang is your locale eg. en, fr etc
===================================== gajim/chat_control.py ===================================== --- a/gajim/chat_control.py +++ b/gajim/chat_control.py @@ -42,7 +42,6 @@ from gajim.common import app from gajim.common import helpers from gajim.common import ged from gajim.common import i18n -from gajim.common.stanza_session import EncryptedStanzaSession, ArchivingStanzaSession from gajim.common.contacts import GC_Contact from nbxmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC from nbxmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO @@ -207,9 +206,6 @@ class ChatControl(ChatControlBase): session.control = self self.session = session
- if session.enable_encryption: - self.print_esession_details() - # Enable encryption if needed self.no_autonegotiation = False self.add_actions() @@ -231,8 +227,6 @@ class ChatControl(ChatControlBase): # Dont connect this when PrivateChatControl is used app.ged.register_event_handler('update-roster-avatar', ged.GUI1, self._nec_update_avatar) - app.ged.register_event_handler('failed-decrypt', ged.GUI1, - self._nec_failed_decrypt) app.ged.register_event_handler('chatstate-received', ged.GUI1, self._nec_chatstate_received) app.ged.register_event_handler('caps-received', ged.GUI1, @@ -920,53 +914,6 @@ class ChatControl(ChatControlBase): chatstate=chatstate_to_send, xhtml=xhtml, process_commands=process_commands, attention=attention)
- def on_cancel_session_negotiation(self): - msg = _('Session negotiation cancelled') - ChatControlBase.print_conversation_line(self, msg, 'status', '', None) - - def print_archiving_session_details(self): - """ - Print esession settings to textview - """ - archiving = bool(self.session) and isinstance(self.session, - ArchivingStanzaSession) and self.session.archiving - if archiving: - msg = _('This session WILL be archived on server') - else: - msg = _('This session WILL NOT be archived on server') - ChatControlBase.print_conversation_line(self, msg, 'status', '', None) - - def print_esession_details(self): - """ - Print esession settings to textview - """ - e2e_is_active = bool(self.session) and self.session.enable_encryption - if e2e_is_active: - msg = _('This session is encrypted') - - if self.session.is_loggable(): - msg += _(' and WILL be logged') - else: - msg += _(' and WILL NOT be logged') - - ChatControlBase.print_conversation_line(self, msg, 'status', '', None) - - if not self.session.verified_identity: - ChatControlBase.print_conversation_line(self, _("Remote contact's identity not verified. Click the shield button for more details."), 'status', '', None) - else: - msg = _('end-to-end encryption disabled') - ChatControlBase.print_conversation_line(self, msg, 'status', '', None) - - self._show_lock_image(e2e_is_active, 'E2E', - self.session and self.session.verified_identity) - - def print_session_details(self, old_session=None): - if isinstance(self.session, EncryptedStanzaSession) or \ - (old_session and isinstance(old_session, EncryptedStanzaSession)): - self.print_esession_details() - elif isinstance(self.session, ArchivingStanzaSession): - self.print_archiving_session_details() - def get_our_nick(self): return app.nicks[self.account]
@@ -1186,8 +1133,6 @@ class ChatControl(ChatControlBase): if self.TYPE_ID == message_control.TYPE_CHAT: app.ged.remove_event_handler('update-roster-avatar', ged.GUI1, self._nec_update_avatar) - app.ged.remove_event_handler('failed-decrypt', ged.GUI1, - self._nec_failed_decrypt) app.ged.remove_event_handler('chatstate-received', ged.GUI1, self._nec_chatstate_received) app.ged.remove_event_handler('caps-received', ged.GUI1, @@ -1482,57 +1427,6 @@ class ChatControl(ChatControlBase): """ dialogs.TransformChatToMUC(self.account, [self.contact.jid])
- def activate_esessions(self): - if not (self.session and self.session.enable_encryption): - self.begin_e2e_negotiation() - - def terminate_esessions(self): - if not (self.session and self.session.enable_encryption): - return - # e2e was enabled, disable it - jid = str(self.session.jid) - thread_id = self.session.thread_id - - self.session.terminate_e2e() - - app.connections[self.account].delete_session(jid, thread_id) - - # presumably the user had a good reason to shut it off, so - # disable autonegotiation too - self.no_autonegotiation = True - - def begin_negotiation(self): - self.no_autonegotiation = True - - if not self.session: - fjid = self.contact.get_full_jid() - new_sess = app.connections[self.account].make_new_session(fjid, type_=self.type_id) - self.set_session(new_sess) - - def begin_e2e_negotiation(self): - self.begin_negotiation() - self.session.resource = self.contact.resource - self.session.negotiate_e2e(False) - - def _nec_failed_decrypt(self, obj): - if obj.session != self.session: - return - - details = _('Unable to decrypt message from %s\nIt may have been ' - 'tampered with.') % obj.fjid - self.print_conversation_line(details, 'status', '', obj.timestamp) - - # terminate the session - thread_id = self.session.thread_id - self.session.terminate_e2e() - obj.conn.delete_session(obj.fjid, thread_id) - - # restart the session - self.begin_e2e_negotiation() - - # Stop emission so it doesn't go to gui_interface - return True - def got_connected(self): ChatControlBase.got_connected(self) # Refreshing contact
===================================== gajim/chat_control_base.py ===================================== --- a/gajim/chat_control_base.py +++ b/gajim/chat_control_base.py @@ -451,9 +451,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): plugin = app.plugin_manager.encryption_plugins[encryption] if not plugin.activate_encryption(self): return - else: - if not self.widget_name == 'groupchat_control': - self.terminate_esessions() + action.set_state(param) self.set_encryption_state(encryption) self.set_encryption_menu_icon()
===================================== gajim/common/app.py ===================================== --- a/gajim/common/app.py +++ b/gajim/common/app.py @@ -147,7 +147,6 @@ caps_hash = {} _dependencies = { 'PYTHON-DBUS': False, 'PYBONJOUR': False, - 'PYCRYPTO': False, 'PYGPG': False, 'GPG_BINARY': False, 'FARSTREAM': False, @@ -188,13 +187,6 @@ def detect_dependencies(): except Exception: pass
- # PYCRYPTO - try: - import Crypto - _dependencies['PYCRYPTO'] = True - except ImportError: - pass - # python-gnupg try: import gnupg
===================================== gajim/common/config.py ===================================== --- a/gajim/common/config.py +++ b/gajim/common/config.py @@ -338,8 +338,6 @@ class Config: 'keyid': [ opt_str, '', '', True ], 'gpg_sign_presence': [ opt_bool, True, _('If disabled, don't sign presences with GPG key, even if GPG is configured.') ], 'keyname': [ opt_str, '', '', True ], - 'enable_esessions': [opt_bool, True, _('Enable ESessions encryption for this account.'), True], - 'autonegotiate_esessions': [opt_bool, False, _('Should Gajim automatically start an encrypted session when possible?')], 'allow_plaintext_connection': [ opt_bool, False, _('Allow plaintext connections')], 'tls_version': [ opt_str, '1.2', '' ], 'cipher_list': [ opt_str, 'HIGH:!aNULL:RC4-SHA', '' ],
===================================== gajim/common/connection.py ===================================== --- a/gajim/common/connection.py +++ b/gajim/common/connection.py @@ -340,10 +340,7 @@ class CommonConnection: # <body> tag is the active event if obj.chatstate and contact and contact.supports(nbxmpp.NS_CHATSTATES): msg_iq.setTag(obj.chatstate, namespace=nbxmpp.NS_CHATSTATES) - only_chatste = False if not obj.message: - only_chatste = True - if only_chatste and not obj.session.enable_encryption: msg_iq.setTag('no-store', namespace=nbxmpp.NS_MSG_HINTS)
===================================== gajim/common/connection_handlers.py ===================================== --- a/gajim/common/connection_handlers.py +++ b/gajim/common/connection_handlers.py @@ -830,8 +830,6 @@ class ConnectionHandlersBase: self._nec_iq_error_received) app.ged.register_event_handler('presence-received', ged.CORE, self._nec_presence_received) - app.ged.register_event_handler('gc-presence-received', ged.CORE, - self._nec_gc_presence_received) app.ged.register_event_handler('message-received', ged.CORE, self._nec_message_received) app.ged.register_event_handler('mam-message-received', ged.CORE, @@ -848,8 +846,6 @@ class ConnectionHandlersBase: self._nec_iq_error_received) app.ged.remove_event_handler('presence-received', ged.CORE, self._nec_presence_received) - app.ged.remove_event_handler('gc-presence-received', ged.CORE, - self._nec_gc_presence_received) app.ged.remove_event_handler('message-received', ged.CORE, self._nec_message_received) app.ged.remove_event_handler('mam-message-received', ged.CORE, @@ -975,24 +971,6 @@ class ConnectionHandlersBase: # resource signs off! self.stop_all_active_file_transfers(obj.contact)
- # disable encryption, since if any messages are - # lost they'll be not decryptable (note that - # this contradicts XEP-0201 - trying to get that - # in the XEP, though) - - # there won't be any sessions here if the contact terminated - # their sessions before going offline (which we do) - for sess in self.get_sessions(jid): - sess_fjid = sess.jid.getStripped() - if sess.resource: - sess_fjid += '/' + sess.resource - if obj.fjid != sess_fjid: - continue - if sess.control: - sess.control.no_autonegotiation = False - if sess.enable_encryption: - sess.terminate_e2e() - if app.config.get('log_contact_status_changes') and \ app.config.should_log(self.name, obj.jid): show = app.logger.convert_show_values_to_db_api_values(obj.show) @@ -1004,15 +982,6 @@ class ConnectionHandlersBase: message=obj.status, show=show)
- def _nec_gc_presence_received(self, obj): - if obj.conn.name != self.name: - return - for sess in self.get_sessions(obj.fjid): - if obj.fjid != sess.jid: - continue - if sess.enable_encryption: - sess.terminate_e2e() - def _nec_message_received(self, obj): if obj.conn.name != self.name: return @@ -1210,14 +1179,7 @@ class ConnectionHandlersBase: except KeyError: return None
- def terminate_sessions(self, send_termination=False): - """ - Send termination messages and delete all active sessions - """ - for jid in self.sessions: - for thread_id in self.sessions[jid]: - self.sessions[jid][thread_id].terminate(send_termination) - + def terminate_sessions(self): self.sessions = {}
def delete_session(self, jid, thread_id):
===================================== gajim/common/connection_handlers_events.py ===================================== --- a/gajim/common/connection_handlers_events.py +++ b/gajim/common/connection_handlers_events.py @@ -2309,17 +2309,6 @@ class Oauth2CredentialsRequiredEvent(nec.NetworkIncomingEvent): name = 'oauth2-credentials-required' base_network_events = []
-class FailedDecryptEvent(nec.NetworkIncomingEvent): - name = 'failed-decrypt' - base_network_events = [] - - def generate(self): - self.conn = self.msg_obj.conn - self.fjid = self.msg_obj.fjid - self.timestamp = self.msg_obj.timestamp - self.session = self.msg_obj.session - return True - class SignedInEvent(nec.NetworkIncomingEvent): name = 'signed-in' base_network_events = []
===================================== gajim/common/crypto.py deleted ===================================== --- a/gajim/common/crypto.py +++ /dev/null @@ -1,105 +0,0 @@ -# common crypto functions (mostly specific to XEP-0116, but useful elsewhere) -# -*- coding:utf-8 -*- -## src/common/crypto.py -## -## Copyright (C) 2007 Brendan Taylor <whateley AT gmail.com> -## -## This file is part of Gajim. -## -## Gajim is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published -## by the Free Software Foundation; version 3 only. -## -## Gajim is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with Gajim. If not, see http://www.gnu.org/licenses/. -## - -import sys -import os -import math - -from hashlib import sha256 as SHA256 - -# convert a large integer to a big-endian bitstring -def encode_mpi(n): - if n >= 256: - return encode_mpi(n // 256) + bytes([n % 256]) - else: - return bytes([n]) - -# convert a large integer to a big-endian bitstring, padded with \x00s to -# a multiple of 16 bytes -def encode_mpi_with_padding(n): - return pad_to_multiple(encode_mpi(n), 16, '\x00', True) - -# pad 'string' to a multiple of 'multiple_of' with 'char'. -# pad on the left if 'left', otherwise pad on the right. -def pad_to_multiple(string, multiple_of, char, left): - mod = len(string) % multiple_of - if mod == 0: - return string - else: - padding = (multiple_of - mod) * char - - if left: - return padding + string - else: - return string + padding - -# convert a big-endian bitstring to an integer -def decode_mpi(s): - if len(s) == 0: - return 0 - else: - return 256 * decode_mpi(s[:-1]) + s[-1] - -def sha256(string): - sh = SHA256() - sh.update(string) - return sh.digest() - -base28_chr = "acdefghikmopqruvwxy123456789" - -def sas_28x5(m_a, form_b): - sha = sha256(m_a + form_b + b'Short Authentication String') - lsb24 = decode_mpi(sha[-3:]) - return base28(lsb24) - -def base28(n): - if n >= 28: - return base28(n // 28) + base28_chr[n % 28] - else: - return base28_chr[n] - -def random_bytes(bytes_): - return os.urandom(bytes_) - -def generate_nonce(): - return random_bytes(8) - -# generate a random number between 'bottom' and 'top' -def srand(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 (decode_mpi(random_bytes(bytes)) % (top - bottom)) + bottom - -# a faster version of (base ** exp) % mod -# taken from http://lists.danga.com/pipermail/yadis/2005-September/001445.html -def powmod(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
===================================== gajim/common/exceptions.py ===================================== --- a/gajim/common/exceptions.py +++ b/gajim/common/exceptions.py @@ -100,12 +100,6 @@ class NegotiationError(Exception): """ pass
-class DecryptionError(Exception): - """ - A message couldn't be decrypted into usable XML - """ - pass - class Cancelled(Exception): """ The user cancelled an operation
===================================== gajim/common/helpers.py ===================================== --- a/gajim/common/helpers.py +++ b/gajim/common/helpers.py @@ -1367,9 +1367,6 @@ def update_optional_features(account = None): app.gajim_optional_features[a].append(nbxmpp.NS_CHATSTATES) if not app.config.get('ignore_incoming_xhtml'): app.gajim_optional_features[a].append(nbxmpp.NS_XHTML_IM) - if app.is_installed('PYCRYPTO') \ - and app.config.get_per('accounts', a, 'enable_esessions'): - app.gajim_optional_features[a].append(nbxmpp.NS_ESESSION) if app.config.get_per('accounts', a, 'answer_receipts'): app.gajim_optional_features[a].append(nbxmpp.NS_RECEIPTS) app.gajim_optional_features[a].append(nbxmpp.NS_JINGLE)
===================================== gajim/common/stanza_session.py deleted ===================================== --- a/gajim/common/stanza_session.py +++ /dev/null @@ -1,1206 +0,0 @@ -# -*- coding:utf-8 -*- -## src/common/stanza_session.py -## -## Copyright (C) 2007-2014 Yann Leboulanger <asterix AT lagaule.org> -## Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com> -## Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com> -## Jean-Marie Traissard <jim AT lapin.org> -## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org> -## -## This file is part of Gajim. -## -## Gajim is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published -## by the Free Software Foundation; version 3 only. -## -## Gajim is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with Gajim. If not, see http://www.gnu.org/licenses/. -## - -from gajim.common import app -import nbxmpp -from gajim.common.exceptions import DecryptionError, NegotiationError -import nbxmpp.c14n - -import itertools -import random -import string -import time -import base64 -import os -from hashlib import sha256 -from hmac import HMAC -from gajim.common import crypto - -import logging -log = logging.getLogger('gajim.c.stanza_session') - -if app.is_installed('PYCRYPTO'): - from Crypto.Cipher import AES - from Crypto.PublicKey import RSA - - from gajim.common import dh - from gajim import secrets - -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_ - self.resource = jid.getResource() - - 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.loggable = True - - self.last_send = 0 - self.last_receive = 0 - self.status = None - self.negotiated = {} - - def is_loggable(self): - return self.loggable and app.config.should_log(self.conn.name, - self.jid.getStripped()) - - def get_to(self): - to = str(self.jid) - return app.get_jid_without_resource(to) + '/' + self.resource - - def remove_events(self, types): - """ - Remove events associated with this session from the queue - - Returns True if any events were removed (unlike events.py remove_events) - """ - any_removed = False - - for j in (self.jid, self.jid.getStripped()): - for event in app.events.get_events(self.conn.name, j, types=types): - # the event wasn't in this session - if (event.type_ == 'chat' and event.session != self) or \ - (event.type_ == 'printed_chat' and event.control.session != \ - self): - continue - - # events.remove_events returns True when there were no events - # for some reason - r = app.events.remove_events(self.conn.name, j, event) - - if not r: - any_removed = True - - return any_removed - - def generate_thread_id(self): - return ''.join([f(string.ascii_letters) for f in itertools.repeat( - random.choice, 32)]) - - def send(self, msg): - if self.thread_id: - msg.NT.thread = self.thread_id - - msg.setAttr('to', self.get_to()) - self.conn.send_stanza(msg) - - if isinstance(msg, nbxmpp.Message): - self.last_send = time.time() - - def reject_negotiation(self, body=None): - msg = nbxmpp.Message() - feature = msg.NT.feature - feature.setNamespace(nbxmpp.NS_FEATURE) - - x = nbxmpp.DataForm(typ='submit') - x.addChild(node=nbxmpp.DataField(name='FORM_TYPE', - value='urn:xmpp:ssn')) - x.addChild(node=nbxmpp.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 - """ - if self.control: - self.control.on_cancel_session_negotiation() - - self.status = None - self.negotiated = {} - - def terminate(self, send_termination = True): - # only send termination message if we've sent a message and think they - # have XEP-0201 support - if send_termination and self.last_send > 0 and \ - (self.received_thread_id or self.last_receive == 0): - msg = nbxmpp.Message() - feature = msg.NT.feature - feature.setNamespace(nbxmpp.NS_FEATURE) - - x = nbxmpp.DataForm(typ='submit') - x.addChild(node=nbxmpp.DataField(name='FORM_TYPE', - value='urn:xmpp:ssn')) - x.addChild(node=nbxmpp.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 to the remote client here - self.status = None - - -class ArchivingStanzaSession(StanzaSession): - def __init__(self, conn, jid, thread_id, type_='chat'): - StanzaSession.__init__(self, conn, jid, thread_id, type_='chat') - self.archiving = False - - def archiving_logging_preference(self, initiator_options=None): - return self.conn.logging_preference(self.jid, initiator_options) - - def negotiate_archiving(self): - self.negotiated = {} - - request = nbxmpp.Message() - feature = request.NT.feature - feature.setNamespace(nbxmpp.NS_FEATURE) - - x = nbxmpp.DataForm(typ='form') - - x.addChild(node=nbxmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn', - typ='hidden')) - x.addChild(node=nbxmpp.DataField(name='accept', value='1', - typ='boolean', required=True)) - - x.addChild(node=nbxmpp.DataField(name='logging', typ='list-single', - options=self.archiving_logging_preference(), required=True)) - - x.addChild(node=nbxmpp.DataField(name='disclosure', typ='list-single', - options=['never'], required=True)) - x.addChild(node=nbxmpp.DataField(name='security', typ='list-single', - options=['none'], required=True)) - - feature.addChild(node=x) - - self.status = 'requested-archiving' - - self.send(request) - - def respond_archiving(self, form): - field = form.getField('logging') - options = [x[1] for x in field.getOptions()] - values = field.getValues() - - logging = self.archiving_logging_preference(options) - self.negotiated['logging'] = logging - - response = nbxmpp.Message() - feature = response.NT.feature - feature.setNamespace(nbxmpp.NS_FEATURE) - - x = nbxmpp.DataForm(typ='submit') - - x.addChild(node=nbxmpp.DataField(name='FORM_TYPE', - value='urn:xmpp:ssn')) - x.addChild(node=nbxmpp.DataField(name='accept', value='true')) - - x.addChild(node=nbxmpp.DataField(name='logging', value=logging)) - - self.status = 'responded-archiving' - - feature.addChild(node=x) - - if not logging: - response = nbxmpp.Error(response, nbxmpp.ERR_NOT_ACCEPTABLE) - - feature = nbxmpp.Node(nbxmpp.NS_FEATURE + ' feature') - - n = nbxmpp.Node('field') - n['var'] = 'logging' - feature.addChild(node=n) - - response.T.error.addChild(node=feature) - - self.send(response) - - def we_accept_archiving(self, form): - if self.negotiated['logging'] == 'mustnot': - self.loggable = False - log.debug('archiving session accepted: %s' % self.loggable) - self.status = 'active' - self.archiving = True - if self.control: - self.control.print_archiving_session_details() - - def archiving_accepted(self, form): - negotiated = {} - ask_user = {} - not_acceptable = [] - - if form['logging'] not in self.archiving_logging_preference(): - raise - - self.negotiated['logging'] = form['logging'] - - accept = nbxmpp.Message() - feature = accept.NT.feature - feature.setNamespace(nbxmpp.NS_FEATURE) - - result = nbxmpp.DataForm(typ='result') - - result.addChild(node=nbxmpp.DataField(name='FORM_TYPE', - value='urn:xmpp:ssn')) - result.addChild(node=nbxmpp.DataField(name='accept', value='1')) - - feature.addChild(node=result) - - self.send(accept) - if self.negotiated['logging'] == 'mustnot': - self.loggable = False - log.debug('archiving session accepted: %s' % self.loggable) - self.status = 'active' - self.archiving = True - if self.control: - self.control.print_archiving_session_details() - - -class EncryptedStanzaSession(ArchivingStanzaSession): - """ - An encrypted stanza negotiation has several states. They arerepresented 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 app.py's - handle_session_negotiation method. - """ - - def __init__(self, conn, jid, thread_id, type_='chat'): - ArchivingStanzaSession.__init__(self, conn, jid, thread_id, - type_='chat') - - 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 - - # has the remote contact's identity ever been verified? - self.verified_identity = False - - def _get_contact(self): - c = app.contacts.get_contact(self.conn.name, self.jid, self.resource) - if not c: - c = app.contacts.get_contact(self.conn.name, self.jid) - return c - - def _is_buggy_gajim(self): - c = self._get_contact() - if c and c.supports(nbxmpp.NS_ROSTERX): - return False - return True - - def set_kc_s(self, value): - """ - Keep the encrypter updated with my latest cipher key - """ - 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 - - def set_kc_o(self, value): - """ - Keep the decrypter updated with the other party's latest cipher key - """ - 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) - - def encryptcounter(self): - self.c_s = (self.c_s + 1) % (2 ** self.n) - return crypto.encode_mpi_with_padding(self.c_s) - - def decryptcounter(self): - self.c_o = (self.c_o + 1) % (2 ** self.n) - return crypto.encode_mpi_with_padding(self.c_o) - - def sign(self, string): - if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'): - hash_ = crypto.sha256(string) - return crypto.encode_mpi(app.pubkey.sign(hash_, '')[0]) - - def encrypt_stanza(self, stanza): - encryptable = [x for x in stanza.getChildren() if x.getName() not in - ('error', 'amp', 'thread')] - - # FIXME 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).decode('utf-8') - - # FIXME check for rekey request, handle <key/> elements - - m_content = (''.join(map(str, c.getChildren()))).encode('utf-8') - c.NT.mac = base64.b64encode(self.hmac(self.km_s, m_content + \ - crypto.encode_mpi(old_en_counter))).decode('utf-8') - - msgtxt = '[This is part of an encrypted session. ' \ - 'If you see this message, something went wrong.]' - lang = os.getenv('LANG') - if lang is not None and lang != 'en': # we're not english - msgtxt = _('[This is part of an encrypted session. ' - 'If you see this message, something went wrong.]') + ' (' + \ - msgtxt + ')' - stanza.setBody(msgtxt) - - return stanza - - def is_xep_200_encrypted(self, msg): - msg.getTag('c', namespace=nbxmpp.NS_STANZA_CRYPTO) - - def hmac(self, key, content): - return HMAC(key, content, self.hash_alg).digest() - - def generate_initiator_keys(self, k): - return (self.hmac(k, b'Initiator Cipher Key'), - self.hmac(k, b'Initiator MAC Key'), - self.hmac(k, b'Initiator SIGMA Key')) - - def generate_responder_keys(self, k): - return (self.hmac(k, b'Responder Cipher Key'), - self.hmac(k, b'Responder MAC Key'), - self.hmac(k, b'Responder SIGMA Key')) - - def compress(self, plaintext): - if self.compression is None: - return plaintext - - def decompress(self, compressed): - if self.compression is None: - return compressed - - def encrypt(self, encryptable): - padded = crypto.pad_to_multiple(encryptable, 16, ' ', False) - - return self.encrypter.encrypt(padded) - - def decrypt_stanza(self, stanza): - """ - Delete the unencrypted explanation body, if it exists - """ - orig_body = stanza.getTag('body') - if orig_body: - stanza.delChild(orig_body) - - 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(str(x) for x in c.getChildren() if x.getName() != 'mac') - macable = macable.encode('utf-8') - - received_mac = base64.b64decode(c.getTagData('mac')) - calculated_mac = self.hmac(self.km_o, macable + \ - crypto.encode_mpi_with_padding(self.c_o)) - - if not calculated_mac == received_mac: - raise DecryptionError('bad signature') - - m_final = base64.b64decode(c.getTagData('data')) - m_compressed = self.decrypt(m_final) - plaintext = self.decompress(m_compressed).decode('utf-8') - - try: - parsed = nbxmpp.Node(node='<node>' + plaintext + '</node>') - except Exception: - raise DecryptionError('decrypted <data/> not parseable as XML') - - for child in parsed.getChildren(): - stanza.addChild(node=child) - - # replace non-character unicode - body = stanza.getBody() - if body: - stanza.setBody( - self.conn.connection.Dispatcher.replace_non_character(body)) - - return stanza - - def decrypt(self, ciphertext): - return self.decrypter.decrypt(ciphertext) - - def logging_preference(self): - if app.config.get_per('accounts', self.conn.name, - '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 NegotiationError('invalid DH value') - - return crypto.sha256(crypto.encode_mpi(crypto.powmod(e, y, p))) - - def c7lize_mac_id(self, form): - kids = form.getChildren() - macable = [x for x in kids if x.getVar() not in ('mac', 'identity')] - return ''.join(nbxmpp.c14n.c14n(el, self._is_buggy_gajim()) for el in \ - 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, crypto.encode_mpi(self.c_o) + id_o) - - if m_o_calculated != m_o: - raise NegotiationError('calculated m_%s differs from received m_%s' % - (i_o, i_o)) - - if i_o == 'a' and self.sas_algs == 'sas28x5': - # we don't need to calculate this if there's a verified retained secret - # (but we do anyways) - self.sas = crypto.sas_28x5(m_o, self.form_s.encode('utf-8')) - - if self.negotiated['recv_pubkey']: - plaintext = self.decrypt(id_o) - parsed = nbxmpp.Node(node='<node>' + plaintext + '</node>') - - if self.negotiated['recv_pubkey'] == 'hash': - # fingerprint = parsed.getTagData('fingerprint') - # FIXME find stored pubkey or terminate session - raise NotImplementedError() - else: - if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'): - keyvalue = parsed.getTag(name='RSAKeyValue', namespace=XmlDsig) - - n, e = (crypto.decode_mpi(base64.b64decode( - keyvalue.getTagData(x))) for x in ('Modulus', 'Exponent')) - eir_pubkey = RSA.construct((n, int(e))) - - pubkey_o = nbxmpp.c14n.c14n(keyvalue, self._is_buggy_gajim()) - else: - # FIXME DSA, etc. - raise NotImplementedError() - - enc_sig = parsed.getTag(name='SignatureValue', - namespace=XmlDsig).getData() - signature = (crypto.decode_mpi(base64.b64decode(enc_sig)), ) - else: - mac_o = self.decrypt(id_o) - pubkey_o = b'' - - c7l_form = self.c7lize_mac_id(form) - - content = self.n_s + self.n_o + crypto.encode_mpi(dh_i) + pubkey_o - - if sigmai: - self.form_o = c7l_form.encode('utf-8') - content += self.form_o - else: - form_o2 = c7l_form.encode('utf-8') - content += self.form_o.encode('utf-8') + form_o2 - - mac_o_calculated = self.hmac(self.ks_o, content) - - if self.negotiated['recv_pubkey']: - hash_ = crypto.sha256(mac_o_calculated) - - if not eir_pubkey.verify(hash_, signature): - raise NegotiationError('public key signature verification failed!') - - elif mac_o_calculated != mac_o: - raise 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'): - pubkey = secrets.secrets().my_pubkey(self.conn.name) - fields = (pubkey.n, pubkey.e) - - cb_fields = [base64.b64encode(crypto.encode_mpi(f)) for f in - fields] - - pubkey_s = b'<RSAKeyValue xmlns="http://www.w3.org/2000/09/xmldsig#"' - '><Modulus>%s</Modulus><Exponent>%s</Exponent></RSAKeyValue>' % \ - tuple(cb_fields) - else: - pubkey_s = b'' - - form_s2 = ''.join(nbxmpp.c14n.c14n(el, self._is_buggy_gajim()) for el \ - in form.getChildren()) - - old_c_s = self.c_s - content = self.n_o + self.n_s + crypto.encode_mpi(dh_i) + pubkey_s + \ - self.form_s.encode('utf-8') + form_s2.encode('utf-8') - - 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, crypto.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 = crypto.sas_28x5(m_s, self.form_o.encode('utf-8')) - - if self.sigmai: - # FIXME save retained secret? - self.check_identity(tuple) - - return (nbxmpp.DataField(name='identity', - value=base64.b64encode(id_s).decode('utf-8')), - nbxmpp.DataField(name='mac', - value=base64.b64encode(m_s).decode('utf-8'))) - - def negotiate_e2e(self, sigmai): - self.negotiated = {} - - request = nbxmpp.Message() - feature = request.NT.feature - feature.setNamespace(nbxmpp.NS_FEATURE) - - x = nbxmpp.DataForm(typ='form') - - x.addChild(node=nbxmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn', - typ='hidden')) - x.addChild(node=nbxmpp.DataField(name='accept', value='1', - typ='boolean', required=True)) - - # this field is incorrectly called 'otr' in XEPs 0116 and 0217 - x.addChild(node=nbxmpp.DataField(name='logging', typ='list-single', - options=self.logging_preference(), required=True)) - - # unsupported options: 'disabled', 'enabled' - x.addChild(node=nbxmpp.DataField(name='disclosure', typ='list-single', - options=['never'], required=True)) - x.addChild(node=nbxmpp.DataField(name='security', typ='list-single', - options=['e2e'], required=True)) - x.addChild(node=nbxmpp.DataField(name='crypt_algs', value='aes128-ctr', - typ='hidden')) - x.addChild(node=nbxmpp.DataField(name='hash_algs', value='sha256', - typ='hidden')) - x.addChild(node=nbxmpp.DataField(name='compress', value='none', - typ='hidden')) - - # unsupported options: 'iq', 'presence' - x.addChild(node=nbxmpp.DataField(name='stanzas', typ='list-multi', - options=['message'])) - - x.addChild(node=nbxmpp.DataField(name='init_pubkey', options=['none', - 'key', 'hash'], typ='list-single')) - - # FIXME store key, use hash - x.addChild(node=nbxmpp.DataField(name='resp_pubkey', options=['none', - 'key'], typ='list-single')) - - x.addChild(node=nbxmpp.DataField(name='ver', value='1.0', typ='hidden')) - - x.addChild(node=nbxmpp.DataField(name='rekey_freq', value='4294967295', - typ='hidden')) - - x.addChild(node=nbxmpp.DataField(name='sas_algs', value='sas28x5', - typ='hidden')) - x.addChild(node=nbxmpp.DataField(name='sign_algs', - value='http://www.w3.org/2000/09/xmldsig#rsa-sha256', typ='hidden')) - - self.n_s = crypto.generate_nonce() - - x.addChild(node=nbxmpp.DataField(name='my_nonce', - value=base64.b64encode(self.n_s).decode('utf-8'), typ='hidden')) - - modp_options = [ int(g) for g in app.config.get('esession_modp').split( - ',') ] - - x.addChild(node=nbxmpp.DataField(name='modp', typ='list-single', - options=[[None, y] for y in modp_options])) - - x.addChild(node=self.make_dhfield(modp_options, sigmai)) - self.sigmai = sigmai - - self.form_s = ''.join(nbxmpp.c14n.c14n(el, self._is_buggy_gajim()) for \ - el in x.getChildren()) - - feature.addChild(node=x) - - self.status = 'requested-e2e' - - self.send(request) - - def verify_options_bob(self, form): - """ - 4.3 esession response (bob) - """ - 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 in form.asDict(): - field = form.getField(name) - options = [x[1] for x in 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: - # FIXME some things are handled elsewhere, some things are - # not-implemented - pass - - return (negotiated, not_acceptable, ask_user) - - def respond_e2e_bob(self, form, negotiated, not_acceptable): - """ - 4.3 esession response (bob) - """ - response = nbxmpp.Message() - feature = response.NT.feature - feature.setNamespace(nbxmpp.NS_FEATURE) - - x = nbxmpp.DataForm(typ='submit') - - x.addChild(node=nbxmpp.DataField(name='FORM_TYPE', - value='urn:xmpp:ssn')) - x.addChild(node=nbxmpp.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=nbxmpp.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 - modp_f = form.getField('modp') - if not modp_f: - return - self.modp = int(modp_f.getOptions()[group_order][1]) - x.addChild(node=nbxmpp.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_f = form.getField('dhhashes') - if not dhhashes_f: - return - dhhashes = dhhashes_f.getValues() - self.negotiated['He'] = base64.b64decode(dhhashes[group_order].encode( - 'utf8')) - - bytes = int(self.n / 8) - - self.n_s = crypto.generate_nonce() - - # n-bit random number - self.c_o = crypto.decode_mpi(crypto.random_bytes(bytes)) - self.c_s = self.c_o ^ (2 ** (self.n - 1)) - - self.y = crypto.srand(2 ** (2 * self.n - 1), p - 1) - self.d = crypto.powmod(g, self.y, p) - - to_add = {'my_nonce': self.n_s, - 'dhkeys': crypto.encode_mpi(self.d), - 'counter': crypto.encode_mpi(self.c_o), - 'nonce': self.n_o} - - for name in to_add: - b64ed = base64.b64encode(to_add[name]).decode('utf-8') - x.addChild(node=nbxmpp.DataField(name=name, value=b64ed)) - - self.form_o = ''.join(nbxmpp.c14n.c14n(el, self._is_buggy_gajim()) for \ - el in form.getChildren()) - self.form_s = ''.join(nbxmpp.c14n.c14n(el, self._is_buggy_gajim()) for \ - el in x.getChildren()) - - self.status = 'responded-e2e' - - feature.addChild(node=x) - - if not_acceptable: - response = nbxmpp.Error(response, nbxmpp.ERR_NOT_ACCEPTABLE) - - feature = nbxmpp.Node(nbxmpp.NS_FEATURE + ' feature') - - for f in not_acceptable: - n = nbxmpp.Node('field') - n['var'] = f - feature.addChild(node=n) - - response.T.error.addChild(node=feature) - - self.send(response) - - def verify_options_alice(self, form): - """ - 'Alice Accepts' - """ - 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) - - def accept_e2e_alice(self, form, negotiated): - """ - 'Alice Accepts', continued - """ - self.encryptable_stanzas = ['message'] - self.sas_algs = 'sas28x5' - self.cipher = AES - self.hash_alg = sha256 - self.compression = None - - self.negotiated = negotiated - - accept = nbxmpp.Message() - feature = accept.NT.feature - feature.setNamespace(nbxmpp.NS_FEATURE) - - result = nbxmpp.DataForm(typ='result') - - self.c_s = crypto.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 = crypto.decode_mpi(base64.b64decode(form['dhkeys'])) - self.k = self.get_shared_secret(self.d, x, p) - - result.addChild(node=nbxmpp.DataField(name='FORM_TYPE', - value='urn:xmpp:ssn')) - result.addChild(node=nbxmpp.DataField(name='accept', value='1')) - result.addChild(node=nbxmpp.DataField(name='nonce', - value=base64.b64encode(self.n_o).decode('utf-8'))) - - 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: - srses = secrets.secrets().retained_secrets(self.conn.name, - self.jid.getStripped()) - rshashes = [self.hmac(self.n_s, rs[0]) for rs in srses] - - if not rshashes: - # we've never spoken before, but we'll pretend we have - rshash_size = self.hash_alg().digest_size - rshashes.append(crypto.random_bytes(rshash_size)) - - rshashes = [base64.b64encode(rshash).decode('utf-8') for rshash in \ - rshashes] - result.addChild(node=nbxmpp.DataField(name='rshashes', - value=rshashes)) - result.addChild(node=nbxmpp.DataField(name='dhkeys', - value=base64.b64encode(crypto.encode_mpi(e)).decode('utf-8'))) - - self.form_o = ''.join(nbxmpp.c14n.c14n(el, self._is_buggy_gajim()) \ - for el in 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' - - def accept_e2e_bob(self, form): - """ - 4.5 esession accept (bob) - """ - response = nbxmpp.Message() - - init = response.NT.init - init.setNamespace(nbxmpp.NS_ESESSION_INIT) - - x = nbxmpp.DataForm(typ='result') - - for field in ('nonce', 'dhkeys', 'rshashes', 'identity', 'mac'): - # FIXME: will do nothing in real world... - assert field in form.asDict(), "alice's form didn't have a %s field" \ - % field - - # 4.5.1 generating provisory session keys - e = crypto.decode_mpi(base64.b64decode(form['dhkeys'])) - p = dh.primes[self.modp] - - if crypto.sha256(crypto.encode_mpi(e)) != self.negotiated['He']: - raise 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 = b'' - - srses = secrets.secrets().retained_secrets(self.conn.name, - self.jid.getStripped()) - rshashes = [base64.b64decode(rshash) for rshash in form.getField( - 'rshashes').getValues()] - - for s in srses: - secret = s[0] - if self.hmac(self.n_o, secret) in rshashes: - srs = secret - break - - # other shared secret - # (we're not using one) - oss = b'' - - k = crypto.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, b'Shared Retained Secret') - else: - srshash = crypto.random_bytes(32) - - x.addChild(node=nbxmpp.DataField(name='FORM_TYPE', - value='urn:xmpp:ssn')) - x.addChild(node=nbxmpp.DataField(name='nonce', value=base64.b64encode( - self.n_o).decode('utf-8'))) - x.addChild(node=nbxmpp.DataField(name='srshash', value=base64.b64encode( - srshash).decode('utf-8'))) - - 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 - - if self.control: - self.control.print_esession_details() - - def final_steps_alice(self, form): - srs = b'' - srses = secrets.secrets().retained_secrets(self.conn.name, - self.jid.getStripped()) - - try: - srshash = base64.b64decode(form['srshash']) - except IndexError: - return - - for s in srses: - secret = s[0] - if self.hmac(secret, b'Shared Retained Secret') == srshash: - srs = secret - break - - oss = b'' - k = crypto.sha256(self.k + srs + oss) - del self.k - - self.do_retained_secret(k, srs) - - # ks_s doesn't need to be calculated 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 - - if self.control: - self.control.print_esession_details() - - def do_retained_secret(self, k, old_srs): - """ - Calculate the new retained secret. determine if the user needs to check - the remote party's identity. Set up callbacks for when the identity has - been verified - """ - new_srs = self.hmac(k, b'New Retained Secret') - self.srs = new_srs - - account = self.conn.name - bjid = self.jid.getStripped() - - self.verified_identity = False - - if old_srs: - if secrets.secrets().srs_verified(account, bjid, old_srs): - # already had a stored secret verified by the user. - secrets.secrets().replace_srs(account, bjid, old_srs, new_srs, True) - # continue without warning. - self.verified_identity = True - else: - # had a secret, but it wasn't verified. - secrets.secrets().replace_srs(account, bjid, old_srs, new_srs, - False) - else: - # we don't even have an SRS - secrets.secrets().save_new_srs(account, bjid, new_srs, False) - - def _verified_srs_cb(self): - secrets.secrets().replace_srs(self.conn.name, self.jid.getStripped(), - self.srs, self.srs, True) - - def _unverified_srs_cb(self): - secrets.secrets().replace_srs(self.conn.name, self.jid.getStripped(), - self.srs, self.srs, False) - - def make_dhfield(self, modp_options, sigmai): - dhs = [] - - for modp in modp_options: - p = dh.primes[modp] - g = dh.generators[modp] - - x = crypto.srand(2 ** (2 * self.n - 1), p - 1) - - # FIXME this may be a source of performance issues - e = crypto.powmod(g, x, p) - - self.xes[modp] = x - self.es[modp] = e - - if sigmai: - dhs.append(base64.b64encode(crypto.encode_mpi(e)).decode('utf-8')) - name = 'dhkeys' - else: - He = crypto.sha256(crypto.encode_mpi(e)) - dhs.append(base64.b64encode(He).decode('utf-8')) - name = 'dhhashes' - - return nbxmpp.DataField(name=name, typ='hidden', value=dhs) - - def terminate_e2e(self): - self.enable_encryption = False - if self.control: - self.control.print_session_details() - self.terminate() - - def acknowledge_termination(self): - StanzaSession.acknowledge_termination(self) - self.enable_encryption = False - - def fail_bad_negotiation(self, reason, fields=None): - """ - Send an error and cancels everything - - If fields is None, the remote party has given us a bad cryptographic - value of some kind. Otherwise, list the fields we haven't implemented. - """ - err = nbxmpp.Error(nbxmpp.Message(), nbxmpp.ERR_FEATURE_NOT_IMPLEMENTED) - err.T.error.T.text.setData(reason) - - if fields: - feature = nbxmpp.Node(nbxmpp.NS_FEATURE + ' feature') - - for field in fields: - fn = nbxmpp.Node('field') - fn['var'] = field - feature.addChild(node=feature) - - err.addChild(node=feature) - - 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 cancelled_negotiation(self): - StanzaSession.cancelled_negotiation(self) - self.enable_encryption = False - self.km_o = ''
===================================== gajim/features_window.py ===================================== --- a/gajim/features_window.py +++ b/gajim/features_window.py @@ -69,10 +69,6 @@ class FeaturesWindow: _('Ability to measure idle time, in order to set auto status.'), _('Requires libxss library.'), _('Requires python2.5.')), - _('End to End message encryption'): (self.pycrypto_available, - _('Encrypting chat messages.'), - _('Requires python-crypto.'), - _('Requires python-crypto.')), _('RST Generator'): (self.docutils_available, _('Generate XHTML output from RST code (see http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html).'), _('Requires python-docutils.'), @@ -166,9 +162,6 @@ class FeaturesWindow: from gajim.common import idle return idle.Monitor.is_available()
- def pycrypto_available(self): - return app.is_installed('PYCRYPTO') - def docutils_available(self): try: __import__('docutils')
===================================== gajim/groupchat_control.py ===================================== --- a/gajim/groupchat_control.py +++ b/gajim/groupchat_control.py @@ -200,13 +200,6 @@ class PrivateChatControl(ChatControl): c = gc_c.as_contact() self.gc_contact = gc_c self.contact = c - if self.session: - # stop e2e - if self.session.enable_encryption: - thread_id = self.session.thread_id - self.session.terminate_e2e() - obj.conn.delete_session(obj.fjid, thread_id) - self.no_autonegotiation = False self.draw_banner() old_jid = obj.room_jid + '/' + obj.nick new_jid = obj.room_jid + '/' + obj.new_nick @@ -282,17 +275,6 @@ class PrivateChatControl(ChatControl): def update_contact(self): self.contact = self.gc_contact.as_contact()
- def begin_e2e_negotiation(self): - self.no_autonegotiation = True - - if not self.session: - fjid = self.gc_contact.get_full_jid() - new_sess = app.connections[self.account].make_new_session(fjid, - type_=self.type_id) - self.set_session(new_sess) - - self.session.negotiate_e2e(False) - def got_disconnected(self): ChatControl.got_disconnected(self)
@@ -1859,20 +1841,6 @@ class GroupchatControl(ChatControlBase): self.nick = obj.new_nick self.new_nick = '' s = _('You are now known as %s') % self.nick - # Stop all E2E sessions - nick_list = app.contacts.get_nick_list(self.account, - self.room_jid) - for nick_ in nick_list: - fjid_ = self.room_jid + '/' + nick_ - ctrl = app.interface.msg_win_mgr.get_control( - fjid_, self.account) - if ctrl and ctrl.session and \ - ctrl.session.enable_encryption: - thread_id = ctrl.session.thread_id - ctrl.session.terminate_e2e() - app.connections[self.account].delete_session( - fjid_, thread_id) - ctrl.no_autonegotiation = False else: s = _('%(nick)s is now known as %(new_nick)s') % { 'nick': nick, 'new_nick': obj.new_nick} @@ -2303,13 +2271,6 @@ class GroupchatControl(ChatControlBase): contact.status = '' ctrl.update_ui() ctrl.parent_win.redraw_tab(ctrl) - for sess in app.connections[self.account].get_sessions(fjid): - if sess.control: - sess.control.no_autonegotiation = False - if sess.enable_encryption: - sess.terminate_e2e() - app.connections[self.account].delete_session(fjid, - sess.thread_id) # They can already be removed by the destroy function if self.room_jid in app.contacts.get_gc_list(self.account): app.contacts.remove_room(self.account, self.room_jid)
===================================== gajim/gui_interface.py ===================================== --- a/gajim/gui_interface.py +++ b/gajim/gui_interface.py @@ -1184,12 +1184,6 @@ class Interface: def handle_atom_entry(obj): AtomWindow.newAtomEntry(obj.atom_entry)
- @staticmethod - def handle_event_failed_decrypt(obj): - details = _('Unable to decrypt message from %s\nIt may have been ' - 'tampered with.') % obj.fjid - dialogs.WarningDialog(_('Unable to decrypt message'), details) - def handle_event_zc_name_conflict(self, obj): def on_ok(new_name): app.config.set_per('accounts', obj.conn.name, 'name', new_name) @@ -1531,7 +1525,6 @@ class Interface: 'client-cert-passphrase': [ self.handle_event_client_cert_passphrase], 'connection-lost': [self.handle_event_connection_lost], - 'failed-decrypt': [(self.handle_event_failed_decrypt, ged.GUI2)], 'file-request-error': [self.handle_event_file_request_error], 'file-request-received': [self.handle_event_file_request], 'gc-invitation-received': [self.handle_event_gc_invitation],
===================================== gajim/message_control.py ===================================== --- a/gajim/message_control.py +++ b/gajim/message_control.py @@ -32,7 +32,6 @@ import uuid from gajim.common import app from gajim.common import helpers from gajim.common import ged -from gajim.common.stanza_session import EncryptedStanzaSession, ArchivingStanzaSession
# Derived types MUST register their type IDs here if custom behavor is required TYPE_CHAT = 'chat' @@ -200,19 +199,6 @@ class MessageControl(object): if session and oldsession: oldsession.control = None
- crypto_changed = bool(session and isinstance(session, - EncryptedStanzaSession) and session.enable_encryption) != \ - bool(oldsession and isinstance(oldsession, EncryptedStanzaSession) \ - and oldsession.enable_encryption) - - archiving_changed = bool(session and isinstance(session, - ArchivingStanzaSession) and session.archiving) != \ - bool(oldsession and isinstance(oldsession, - ArchivingStanzaSession) and oldsession.archiving) - - if crypto_changed or archiving_changed: - self.print_session_details(oldsession) - def remove_session(self, session): if session != self.session: return
===================================== gajim/secrets.py deleted ===================================== --- a/gajim/secrets.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding:utf-8 -*- -## src/secrets.py -## -## Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com> -## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org> -## -## This file is part of Gajim. -## -## Gajim is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published -## by the Free Software Foundation; version 3 only. -## -## Gajim is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with Gajim. If not, see http://www.gnu.org/licenses/. -## - -from gajim.common import configpaths - -import Crypto -from gajim.common import crypto -from gajim.common import exceptions - -import os -import pickle - -secrets_filename = configpaths.get('SECRETS_FILE') -secrets_cache = None - - -class Secrets(): - def __init__(self, filename): - self.filename = filename - self.srs = {} - self.pubkeys = {} - self.privkeys = {} - - def cancel(self): - raise exceptions.Cancelled - - def save(self): - f = open(secrets_filename, 'wb') - pickle.dump(self, f, protocol=2) - f.close() - - def retained_secrets(self, account, bare_jid): - try: - return self.srs[account][bare_jid] - except KeyError: - return [] - - # retained secrets are stored as a tuple of the secret and whether the user - # has verified it - def save_new_srs(self, account, jid, secret, verified): - if not account in self.srs: - self.srs[account] = {} - - if not jid in self.srs[account]: - self.srs[account][jid] = [] - - self.srs[account][jid].append((secret, verified)) - - self.save() - - def find_srs(self, account, jid, srs): - our_secrets = self.srs[account][jid] - return [(x, y) for x, y in our_secrets if x == srs][0] - - # has the user verified this retained secret? - def srs_verified(self, account, jid, srs): - return self.find_srs(account, jid, srs)[1] - - def replace_srs(self, account, jid, old_secret, new_secret, verified): - our_secrets = self.srs[account][jid] - - idx = our_secrets.index(self.find_srs(account, jid, old_secret)) - - our_secrets[idx] = (new_secret, verified) - - self.save() - - # the public key associated with 'account' - def my_pubkey(self, account): - try: - pk = self.privkeys[account] - except KeyError: - pk = Crypto.PublicKey.RSA.generate(2048, crypto.random_bytes) - - self.privkeys[account] = pk - self.save() - - return pk - -def load_secrets(filename): - f = open(filename, 'rb') - - try: - secrets = pickle.load(f, encoding='latin1') - # We do that to be able to read files written in py2 - for acct in secrets.srs: - for jid in secrets.srs[acct]: - for (secret, verified) in list(secrets.srs[acct][jid]): - if type(secret) is str: - secrets.srs[acct][jid].remove((secret, verified)) - secrets.srs[acct][jid].append((secret.encode('latin1'), verified)) - except (KeyError, EOFError, ImportError): - f.close() - secrets = Secrets(filename) - - f.close() - return secrets - -def secrets(): - global secrets_cache - - if secrets_cache: - return secrets_cache - - if os.path.exists(secrets_filename): - secrets_cache = load_secrets(secrets_filename) - else: - secrets_cache = Secrets(secrets_filename) - - return secrets_cache
===================================== gajim/session.py ===================================== --- a/gajim/session.py +++ b/gajim/session.py @@ -21,11 +21,13 @@ ## along with Gajim. If not, see http://www.gnu.org/licenses/. ##
+import string +import random +import itertools + from gajim.common import helpers from gajim.common import events -from gajim.common import exceptions from gajim.common import app -from gajim.common import stanza_session from gajim.common import contacts from gajim.common import ged from gajim.common.connection_handlers_events import ChatstateReceivedEvent, \ @@ -34,28 +36,48 @@ from gajim.common.const import KindConstant from gajim import message_control from gajim import notify from gajim import dialogs -from gajim import negotiation
-class ChatControlSession(stanza_session.EncryptedStanzaSession): - def __init__(self, conn, jid, thread_id, type_='chat'): - stanza_session.EncryptedStanzaSession.__init__(self, conn, jid, thread_id, - type_='chat') - app.ged.register_event_handler('decrypted-message-received', ged.PREGUI, - self._nec_decrypted_message_received)
+class ChatControlSession(object): + def __init__(self, conn, jid, thread_id, type_='chat'): + self.conn = conn + self.jid = jid + self.type_ = type_ + self.resource = jid.getResource() self.control = None
- def detach_from_control(self): - if self.control: - self.control.set_session(None) + 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.loggable = True
- def acknowledge_termination(self): - self.detach_from_control() - stanza_session.EncryptedStanzaSession.acknowledge_termination(self) + self.last_send = 0 + self.last_receive = 0
- def terminate(self, send_termination = True): - stanza_session.EncryptedStanzaSession.terminate(self, send_termination) - self.detach_from_control() + app.ged.register_event_handler('decrypted-message-received', + ged.PREGUI, + self._nec_decrypted_message_received) + + def generate_thread_id(self): + return ''.join( + [f(string.ascii_letters) for f in itertools.repeat( + random.choice, 32)] + ) + + def is_loggable(self): + return app.config.should_log(self.conn.name, + self.jid.getStripped()) + + def get_to(self): + to = str(self.jid) + return app.get_jid_without_resource(to) + '/' + self.resource
def _nec_decrypted_message_received(self, obj): """ @@ -400,177 +422,3 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): else: bb_jid, bb_account = jid, self.conn.name app.interface.roster.select_contact(bb_jid, bb_account) - - # ---- ESessions stuff --- - - def handle_negotiation(self, form): - if form.getField('accept') and not form['accept'] in ('1', 'true'): - self.cancelled_negotiation() - return - - # encrypted session states. these are described in stanza_session.py - - try: - if form.getType() == 'form' and 'security' in form.asDict(): - security_options = [x[1] for x in form.getField('security').\ - getOptions()] - if security_options == ['none']: - self.respond_archiving(form) - else: - # bob responds - - # we don't support 3-message negotiation as the responder - if 'dhkeys' in form.asDict(): - self.fail_bad_negotiation('3 message negotiation not ' - 'supported when responding', ('dhkeys',)) - return - - negotiated, not_acceptable, ask_user = \ - self.verify_options_bob(form) - - if ask_user: - def accept_nondefault_options(is_checked): - self.dialog.destroy() - negotiated.update(ask_user) - self.respond_e2e_bob(form, negotiated, - not_acceptable) - - def reject_nondefault_options(): - self.dialog.destroy() - for key in ask_user.keys(): - not_acceptable.append(key) - self.respond_e2e_bob(form, negotiated, - not_acceptable) - - self.dialog = dialogs.YesNoDialog(_('Confirm these ' - 'session options'), - _('The remote client wants to negotiate a session ' - 'with these features:\n\n%s\n\nAre these options ' - 'acceptable?''') % ( - negotiation.describe_features(ask_user)), - on_response_yes=accept_nondefault_options, - on_response_no=reject_nondefault_options, - transient_for=self.control.parent_win.window) - else: - self.respond_e2e_bob(form, negotiated, not_acceptable) - - return - - elif self.status == 'requested-archiving' and form.getType() == \ - 'submit': - try: - self.archiving_accepted(form) - except exceptions.NegotiationError as details: - self.fail_bad_negotiation(details) - - return - - # alice accepts - elif self.status == 'requested-e2e' and form.getType() == 'submit': - negotiated, not_acceptable, ask_user = self.verify_options_alice( - form) - - if ask_user: - def accept_nondefault_options(is_checked): - if dialog: - dialog.destroy() - - if is_checked: - allow_no_log_for = app.config.get_per( - 'accounts', self.conn.name, - 'allow_no_log_for').split() - jid = str(self.jid) - if jid not in allow_no_log_for: - allow_no_log_for.append(jid) - app.config.set_per('accounts', self.conn.name, - 'allow_no_log_for', ' '.join(allow_no_log_for)) - - negotiated.update(ask_user) - - try: - self.accept_e2e_alice(form, negotiated) - except exceptions.NegotiationError as details: - self.fail_bad_negotiation(details) - - def reject_nondefault_options(): - self.reject_negotiation() - dialog.destroy() - - allow_no_log_for = app.config.get_per('accounts', - self.conn.name, 'allow_no_log_for').split() - if str(self.jid) in allow_no_log_for: - dialog = None - accept_nondefault_options(False) - else: - 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)), - _('Always accept for this contact'), - on_response_yes = accept_nondefault_options, - on_response_no = reject_nondefault_options, - transient_for=self.control.parent_win.window) - else: - try: - self.accept_e2e_alice(form, negotiated) - except exceptions.NegotiationError as details: - self.fail_bad_negotiation(details) - - return - elif self.status == 'responded-archiving' and form.getType() == \ - 'result': - try: - self.we_accept_archiving(form) - except exceptions.NegotiationError as details: - self.fail_bad_negotiation(details) - - return - elif self.status == 'responded-e2e' and form.getType() == 'result': - try: - self.accept_e2e_bob(form) - except exceptions.NegotiationError as details: - self.fail_bad_negotiation(details) - - return - elif self.status == 'identified-alice' and form.getType() == 'result': - try: - self.final_steps_alice(form) - except exceptions.NegotiationError as details: - self.fail_bad_negotiation(details) - - return - except exceptions.Cancelled: - # user cancelled the negotiation - - self.reject_negotiation() - - return - - if form.getField('terminate') and\ - form.getField('terminate').getValue() in ('1', 'true'): - self.acknowledge_termination() - - self.conn.delete_session(str(self.jid), self.thread_id) - - return - - # non-esession negotiation. this isn't very useful, but i'm keeping it - # around to test my test suite. - if form.getType() == 'form': - if not self.control: - jid, resource = app.get_room_and_nick_from_fjid(str(self.jid)) - - account = self.conn.name - contact = app.contacts.get_contact(account, str(self.jid), - resource) - - if not contact: - contact = app.contacts.create_contact(jid=jid, account=account, - resource=resource, show=self.conn.get_status()) - - app.interface.new_chat(contact, account, resource=resource, - session=self) - - negotiation.FeatureNegotiationWindow(account, str(self.jid), self, - form)
===================================== org.gajim.Gajim.json ===================================== --- a/org.gajim.Gajim.json +++ b/org.gajim.Gajim.json @@ -212,25 +212,6 @@ } ] }, - /* ESession support */ - { - "name": "python3-pycrypto", - "buildsystem": "simple", - "build-commands": [ - "python3 setup.py install --prefix=/app" - ], - "sources": [ - { - "type": "archive", - "url": "https://pypi.python.org/packages/60/db/645aa9af249f059cc3a368b118de33889219e...", - "sha256": "f2ce1e989b272cfcb677616763e0a2e7ec659effa67a88aa92b3a65528f60a3c" - }, - { - "type": "patch", - "path": "flatpak/CVE-2013-7459.patch" - } - ] - }, /* gnupg support */ { "name": "python3-python-gnupg",
===================================== test/test_pluginmanager.py ===================================== --- a/test/test_pluginmanager.py +++ b/test/test_pluginmanager.py @@ -57,8 +57,6 @@ configpaths.init()
configpaths.override_path('DATA', gajim_root + '/gajim/data')
-from common.stanza_session import StanzaSession - # name to use for the test account account_name = 'test'
===================================== test/unit/test_sessions.py ===================================== --- a/test/unit/test_sessions.py +++ b/test/unit/test_sessions.py @@ -16,7 +16,6 @@ from gajim.common.connection_handlers_events import MessageReceivedEvent from gajim.common.connection_handlers_events import DecryptedMessageReceivedEvent import nbxmpp
-from gajim.common.stanza_session import StanzaSession from gajim.session import ChatControlSession from gajim.roster_window import RosterWindow
@@ -30,59 +29,6 @@ app.interface = MockInterface() # name to use for the test account account_name = account1
-class TestStanzaSession(unittest.TestCase): - ''' Testclass for common/stanzasession.py ''' - - def setUp(self): - self.jid = nbxmpp.JID('test@example.org/Gajim') - self.conn = MockConnection(account_name, {'send_stanza': None}) - self.sess = StanzaSession(self.conn, self.jid, None, 'chat') - - def test_generate_thread_id(self): - # thread_id is a string - self.assertTrue(isinstance(self.sess.thread_id, str)) - - # it should be somewhat long, to avoid clashes - self.assertTrue(len(self.sess.thread_id) >= 32) - - def test_is_loggable(self): - # by default a session should be loggable - # (unless the no_log_for setting says otherwise) - self.assertTrue(self.sess.is_loggable()) - - def test_terminate(self): - # termination is sent by default - self.sess.last_send = time.time() - self.sess.terminate() - - self.assertEqual(None, self.sess.status) - - calls = self.conn.mockGetNamedCalls('send_stanza') - msg = calls[0].getParam(0) - - self.assertEqual(msg.getThread(), self.sess.thread_id) - - def test_terminate_without_sending(self): - # no termination is sent if no messages have been sent in the session - self.sess.terminate() - - self.assertEqual(None, self.sess.status) - - calls = self.conn.mockGetNamedCalls('send_stanza') - self.assertEqual(0, len(calls)) - - def test_terminate_no_remote_xep_201(self): - # no termination is sent if only messages without thread ids have been - # received - self.sess.last_send = time.time() - self.sess.last_receive = time.time() - self.sess.terminate() - - self.assertEqual(None, self.sess.status) - - calls = self.conn.mockGetNamedCalls('send_stanza') - self.assertEqual(0, len(calls)) -
class TestChatControlSession(unittest.TestCase): ''' Testclass for session.py '''
===================================== win/_base.sh ===================================== --- a/win/_base.sh +++ b/win/_base.sh @@ -91,7 +91,6 @@ function install_deps {
PIP_REQUIREMENTS="\ git+https://dev.gajim.org/gajim/python-nbxmpp.git -git+https://github.com/dlitz/pycrypto.git git+https://dev.gajim.org/lovetox/pybonjour-python3.git keyring python-gnupg
===================================== win/dev_env.sh ===================================== --- a/win/dev_env.sh +++ b/win/dev_env.sh @@ -23,7 +23,6 @@ certifi git+https://dev.gajim.org/gajim/python-nbxmpp.git git+https://dev.gajim.org/lovetox/pybonjour-python3.git protobuf -git+https://github.com/dlitz/pycrypto.git cryptography pyopenssl python-gnupg
View it on GitLab: https://dev.gajim.org/gajim/gajim/commit/32b74b459e49abbef8aefcd6a417fab1c8b...
--- View it on GitLab: https://dev.gajim.org/gajim/gajim/commit/32b74b459e49abbef8aefcd6a417fab1c8b... You're receiving this email because of your account on dev.gajim.org.