Skip to content
Snippets Groups Projects
otrmodule.py 27.7 KiB
Newer Older
#!/usr/bin/env python
# -*- coding: utf-8 -*-
##    otrmodule.py
##
## Copyright 2008-2012 Kjell Braden <afflux@pentabarf.de>
##
## 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/>.
##


'''
Off-The-Record encryption plugin.

:author: Kjell self.Braden <kb.otr@pentabarf.de>
:since: 2008
:copyright: Copyright 2008-2012 Kjell Braden <afflux@pentabarf.de>
MINVERSION = (1,0,0,'beta5')
MINCRYPTOVERSION = (2,1,0,'final',0)
IGNORE = True
PASS = False

DEFAULTFLAGS = {
            'ALLOW_V1':False,
            'ALLOW_V2':True,
            'REQUIRE_ENCRYPTION':False,
            'SEND_TAG':True,
            'WHITESPACE_START_AKE':True,
            'ERROR_START_AKE':True,
Dicson's avatar
Dicson committed
        }
MINVERSION_OUTGOING_MSG_STAZA = "0.16.4"

enc_tip = 'A private chat session <i>is established</i> to this contact ' \
        'with this fingerprint'
unused_tip = 'A private chat session is established to this contact using ' \
        '<i>another</i> fingerprint'
ended_tip = 'The private chat session to this contact has <i>ended</i>'
inactive_tip = 'Communication to this contact is currently ' \
        '<i>unencrypted</i>'
msg_not_send = _('Your message was not send. Either end '
                 'your private conversation, or restart it')

Kjell Braden's avatar
Kjell Braden committed
import logging
from distutils.version import LooseVersion

from common import gajim
from common import ged
from common.connection_handlers_events import MessageOutgoingEvent
from plugins import GajimPlugin
from message_control import TYPE_CHAT, MessageControl
from plugins.helpers import log_calls, log
sys.path.insert(0, os.path.dirname(ui.__file__))

from HTMLParser import HTMLParser
from htmlentitydefs import name2codepoint
name2codepoint['apos'] = 0x0027
HAS_CRYPTO = True
try:
    import Crypto
    if not hasattr(Crypto, 'version_info') \
    or Crypto.version_info < MINCRYPTOVERSION:
        raise ImportError('PyCrypto not found or too old')
except ImportError:
    HAS_CRYPTO = False
    import potr.crypt
    import potr.context
    if not hasattr(potr, 'VERSION') or potr.VERSION < MINVERSION:
        raise ImportError('old / unsupported python-otr version')
Kjell Braden's avatar
Kjell Braden committed
    potrrootlog = logging.getLogger('potr')
    potrrootlog.handlers = []
    potrrootlog.propagate = False
    gajimrootlog = logging.getLogger('gajim')
    for h in gajimrootlog.handlers:
        potrrootlog.addHandler(h)

        return gajim.get_room_and_nick_from_fjid(str(fjid))[0]

    class GajimContext(potr.context.Context):
        # self.peer is fjid
        # self.jid does not contain resource
        __slots__ = ['smpWindow', 'jid']

        def __init__(self, account, peer):
            super(GajimContext, self).__init__(account, peer)
            self.jid = get_jid_from_fjid(peer)
            self.trustName = self.jid
            self.smpWindow = ui.ContactOtrSmpWindow(self)

        def inject(self, msg, appdata=None):
            log.debug('inject(appdata=%s)', appdata)
            msg = unicode(msg)
            account = self.user.accountname

            stanza = nbxmpp.Message(to=self.peer, body=msg, typ='chat')
            if appdata is not None:
                session = appdata.get('session', None)
                if session is not None:
                    stanza.setThread(session.thread_id)
            add_message_processing_hints(stanza)
            gajim.connections[account].connection.send(stanza, now=True)

        def setState(self, newstate):
            if self.state == potr.context.STATE_ENCRYPTED:
                # we were encrypted
                if newstate == potr.context.STATE_ENCRYPTED:
                    # and are still -> it's just a refresh
                    OtrPlugin.gajim_log(
                            _('Private conversation with %s refreshed.') % self.peer,
Dicson's avatar
Dicson committed
                            self.user.accountname, self.peer)
                elif newstate == potr.context.STATE_FINISHED:
                    # and aren't anymore -> other side disconnected
                    OtrPlugin.gajim_log(_('%s has ended his/her private '
                            'conversation with you. You should do the same.')
                            % self.peer, self.user.accountname, self.peer)
            else:
                if newstate == potr.context.STATE_ENCRYPTED:
                    # we are now encrypted
                    trust = self.getCurrentTrust()
                    if trust is None:
                        fpr = str(self.getCurrentKey())
                        OtrPlugin.gajim_log(_('New fingerprint for %(peer)s: %(fpr)s')
                                % {'peer': self.peer, 'fpr': fpr},
                                self.user.accountname, self.peer)
                        self.setCurrentTrust('')
                    trustStr = 'authenticated' if bool(trust) else '*unauthenticated*'
                    OtrPlugin.gajim_log(
                        _('%(trustStr)s secured OTR conversation with %(peer)s started')
                        % {'trustStr': trustStr, 'peer': self.peer},
                        self.user.accountname, self.peer)

            if self.state != potr.context.STATE_PLAINTEXT and \
                    newstate == potr.context.STATE_PLAINTEXT:
                # we are now plaintext
                        _('Private conversation with %s lost.') % self.peer,
                        self.user.accountname, self.peer)

            super(GajimContext, self).setState(newstate)
            OtrPlugin.update_otr(self.peer, self.user.accountname)
            self.user.plugin.update_context_list()

        def getPolicy(self, key):
            ret = self.user.plugin.get_flags(self.user.accountname, self.jid)[key]
            log.debug('getPolicy(key=%s) = %s', key, ret)
            return ret

    class GajimOtrAccount(potr.context.Account):
        contextclass = GajimContext
        def __init__(self, plugin, accountname):
            global PROTOCOL, MMS
            self.plugin = plugin
            self.accountname = accountname
            name = gajim.get_jid_from_account(accountname)
            super(GajimOtrAccount, self).__init__(name, PROTOCOL, MMS)
            self.keyFilePath = os.path.join(gajim.gajimpaths.data_root, accountname)

        def dropPrivkey(self):
            try:
                os.remove(self.keyFilePath + '.key3')
            except IOError, e:
                if e.errno != 2:
                    log.exception('IOError occurred when removing key file for %s',
                            self.name)
            self.privkey = None

        def loadPrivkey(self):
            try:
                with open(self.keyFilePath + '.key3', 'rb') as keyFile:
                    return potr.crypt.PK.parsePrivateKey(keyFile.read())[0]
            except IOError, e:
                if e.errno != 2:
                    log.exception('IOError occurred when loading key file for %s',
                            self.name)
            return None

        def savePrivkey(self):
            try:
                with open(self.keyFilePath + '.key3', 'wb') as keyFile:
                    keyFile.write(self.getPrivkey().serializePrivateKey())
            except IOError, e:
                log.exception('IOError occurred when loading key file for %s',
                        self.name)

        def loadTrusts(self, newCtxCb=None):
            ''' load the fingerprint trustdb '''
            # it has the same format as libotr, therefore the
            # redundant account / proto field
            try:
                with open(self.keyFilePath + '.fpr', 'r') as fprFile:
                    for line in fprFile:
                        ctx, acc, proto, fpr, trust = line[:-1].split('\t')

                        if acc != self.name or proto != PROTOCOL:
                            continue

                        jid = get_jid_from_fjid(ctx)
                        self.setTrust(jid, fpr, trust)
            except IOError, e:
                if e.errno != 2:
                    log.exception('IOError occurred when loading fpr file for %s',
                            self.name)

        def saveTrusts(self):
            try:
                with open(self.keyFilePath + '.fpr', 'w') as fprFile:
                    for uid, trusts in self.trusts.iteritems():
                        for fpr, trustVal in trusts.iteritems():
                            fprFile.write('\t'.join(
                                    (uid, self.name, PROTOCOL, fpr, trustVal)))
                            fprFile.write('\n')
            except IOError, e:
                log.exception('IOError occurred when loading fpr file for %s',
                        self.name)
except ImportError:
    HAS_POTR = False

def otr_dialog_destroy(widget, *args, **kwargs):
    widget.destroy()

class OtrPlugin(GajimPlugin):
    otr = None
    def init(self):

        self.us = {}


        if not HAS_POTR:
            self.activatable = False
            self.available_text = _('Can\'t find potr. Verify this ' \
                    'plugin\'s integrity.')
            return

        if not HAS_CRYPTO:
            self.activatable = False
            self.available_text = _('PyCrypto not installed or too old.')
            return


        self.config_dialog = ui.OtrPluginConfigDialog(self)
        self.events_handlers = {}
        self.events_handlers['message-received'] = (ged.PRECORE,
                self.handle_incoming_msg)
        self.events_handlers['before-change-show'] = (ged.PRECORE,
                self.handle_change_show)
        if LooseVersion(gajim.config.get('version')) < LooseVersion(MINVERSION_OUTGOING_MSG_STAZA):
            self.events_handlers['message-outgoing'] = (ged.OUT_PRECORE,
                    self.handle_outgoing_msg)
        else:
            self.events_handlers['stanza-message-outgoing'] = (ged.OUT_PRECORE,
                    self.handle_outgoing_msg_stanza)

        self.gui_extension_points = {
                    'chat_control' : (self.cc_connect, self.cc_disconnect)
                }

        for acc in gajim.contacts.get_accounts():
            self.us[acc] = GajimOtrAccount(self, acc)
            self.us[acc].loadTrusts()

            acc = str(acc)
            if acc not in self.config or None not in self.config[acc]:
                self.config[acc] = {None:DEFAULTFLAGS.copy()}
        self.update_context_list()
        if not HAS_CRYPTO or not HAS_POTR or not hasattr(potr, 'VERSION') \
        or potr.VERSION < MINVERSION:
            raise GajimPluginException(self.available_text)
    def get_otr_status(self, account, contact):
        ctx = self.us[account].getContext(contact.get_full_jid())

        finished = ctx.state == potr.context.STATE_FINISHED
        encrypted = finished or ctx.state == potr.context.STATE_ENCRYPTED
        trusted = encrypted and bool(ctx.getCurrentTrust())
        return (encrypted, trusted, finished)

    def cc_connect(self, cc):
        def update_otr(print_status=False):
            enc_status, authenticated, finished = \
                    self.get_otr_status(cc.account, cc.contact)
            otr_status_text = ''

            if finished:
                otr_status_text = u'finished OTR connection'
            elif authenticated:
                otr_status_text = u'authenticated secure OTR connection'
            elif enc_status:
                otr_status_text = u'*unauthenticated* secure OTR connection'

            cc._show_lock_image(enc_status, u'OTR', enc_status, True,
                    authenticated)
            if print_status and otr_status_text:
                cc.print_conversation_line(u'[OTR] %s' % otr_status_text,
                        'status', '', None)
        cc.update_otr = update_otr
        cc.update_otr(True)

        # hijack authentication button with our submenu
        def authbutton_cb(widget):
            if not cc.gpg_is_active and not (cc.session and
            cc.session.enable_encryption):
                ui.get_otr_submenu(self, cc).get_submenu().popup(None,
                        None, None, 0, 0)
            else:
                cc._on_authentication_button_clicked(widget)
        self.overwrite_handler(cc, cc.authentication_button, authbutton_cb)

        # hijack context menu
        cc.orig_prepare_context_menu = cc.prepare_context_menu
        def inject_menu(hide_buttonbar_items=False):
            menu = cc.orig_prepare_context_menu(hide_buttonbar_items)
            menu.insert(ui.get_otr_submenu(self, cc), 8)
            return menu
        cc.prepare_context_menu = inject_menu

    def cc_disconnect(self, cc):
        try:
            self.overwrite_handler(cc, cc.authentication_button,
                    cc._on_authentication_button_clicked)
            cc.prepare_context_menu = cc.orig_prepare_context_menu
            del cc.update_otr
        except AttributeError:
            pass

    def menu_settings_cb(self, item, control):
        ctx = self.us[control.account].getContext(control.contact.get_full_jid())
        dlg = ui.ContactOtrWindow(self, ctx)
        dlg.run()
        dlg.destroy()

    def menu_start_cb(self, item, control):
        gajim.nec.push_outgoing_event(MessageOutgoingEvent(None,
                account=control.account, jid=control.contact.jid,
                message=u'?OTRv?', type_='chat',
                resource=control.contact.resource, is_loggable=False))

    def menu_end_cb(self, item, control):
        fjid = control.contact.get_full_jid()
        thread_id = control.session.thread_id if control.session else None

        self.us[control.account].getContext(fjid).disconnect(
                appdata={'session':control.session})

    def menu_smp_cb(self, item, control):
        ctx = self.us[control.account].getContext(control.contact.get_full_jid())
        ctx.smpWindow.show(False)

    @staticmethod
    def overwrite_handler(window, control, handler):
        for id_, v in window.handlers.iteritems():
            if v == control:
                break
        else:
            raise LookupError

        del window.handlers[id_]
        control.disconnect(id_)
        id_ = control.connect('clicked', handler)
        window.handlers[id_] = control

    def set_flags(self, value, account=None, contact=None):
        if isinstance(account, unicode):
            account = account.encode()

        if account not in self.config:
            self.config[account] = {None:DEFAULTFLAGS.copy()}

        if account is None and contact is not None:
            # don't set per-contact options without account
            raise Exception("can't set contact flags without account")

        config = self.config[account]
        config[contact] = value

        self.config[account] = config

    def get_flags(self, account=None, contact=None, fallback=True):
        if isinstance(account, unicode):
            account = account.encode()

        setting = DEFAULTFLAGS.copy()
        if account in self.config:
            setting.update(self.config[account][None])
            if contact in self.config[account] \
                    and self.config[account][contact] is not None:
                setting.update(self.config[account][contact])
            elif not fallback:
                return None
        return setting

    def update_context_list(self):
        self.config_dialog.fpr_model.clear()
        for us in self.us.itervalues():
            usedFpr = set()
            for fjid, ctx in us.ctxs.iteritems():
                # get active contexts first
                key = ctx.getCurrentKey()
                if not key:
                    continue
                fpr = key.cfingerprint()
                usedFpr.add(fpr)

                human_hash = potr.human_hash(fpr)
                trust = bool(us.getTrust(ctx.trustName, fpr))

                if ctx.state == potr.context.STATE_ENCRYPTED:
                    state = "encrypted"
                    tip = enc_tip
                elif ctx.state == potr.context.STATE_FINISHED:
                    state = "finished"
                    tip = ended_tip
                else:
                    state = 'inactive'
                    tip = inactive_tip

                self.config_dialog.fpr_model.append((fjid, state, trust,
                        '<tt>%s</tt>' % human_hash, us.name, tip, fpr))

            for uid, trusts in us.trusts.iteritems():
                for fpr, trust in trusts.iteritems():
                    if fpr in usedFpr:
                        continue

                    state = 'inactive'
                    tip = inactive_tip
                    self.config_dialog.fpr_model.append((uid, state, bool(trust),
                            '<tt>%s</tt>' % human_hash, us.name, tip, fpr))

    @classmethod
    def gajim_log(cls, msg, account, fjid, no_print=False,
    is_status_message=True, thread_id=None):
        if not isinstance(fjid, unicode):
            fjid = unicode(fjid)
        if not isinstance(account, unicode):
            account = unicode(account)

        resource = gajim.get_resource_from_jid(fjid)
        jid = gajim.get_jid_without_resource(fjid)
        tim = time.localtime()

        if is_status_message is True:
            if not no_print:
                ctrl = cls.get_control(fjid, account)
                if ctrl:
                    ctrl.print_conversation_line(u'[OTR] %s' % msg, 'status',
                            '', None)
            if gajim.config.should_log(account, jid):
                id = gajim.logger.write('chat_msg_recv', fjid,
                        message=u'[OTR: %s]' % msg, tim=tim)
                # gajim.logger.write() only marks a message as unread (and so
                # only returns an id) when fjid is a real contact (NOT if it's a
                # GC private chat)
                if id:
                    gajim.logger.set_read_messages([id])
        else:
            session = gajim.connections[account].get_or_create_session(fjid,
                    thread_id)
            session.received_thread_id |= bool(thread_id)
            session.last_receive = time.time()

            if not session.control:
                # look for an existing chat control without a session
                ctrl = cls.get_control(fjid, account)
                if ctrl:
                    session.control = ctrl
                    session.control.set_session(session)

            if gajim.config.should_log(account, jid):
                msg_id = gajim.logger.write('chat_msg_recv', fjid,
                        message=u'[OTR: %s]' % msg, tim=tim)
                session.roster_message(jid, msg, tim=tim, msg_id=msg_id,
                        msg_type='chat', resource=resource)

    @classmethod
    def update_otr(cls, user, acc, print_status=False):
        ctrl = cls.get_control(user, acc)
        if ctrl:
            ctrl.update_otr(print_status)

    @staticmethod
    def get_control(fjid, account):
        # first try to get the window with the full jid
        ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account)
        if ctrl:
            # got one, be happy
            return ctrl

        # otherwise try without the resource
        ctrl = gajim.interface.msg_win_mgr.get_control(
                gajim.get_jid_without_resource(fjid), account)
        # but only use it when it's not a GC window
        if ctrl and ctrl.TYPE_ID == TYPE_CHAT:
            return ctrl

    def handle_change_show(self, event):
        account = event.conn.name

        if event.show == 'offline':
            for us in self.us.itervalues():
                for fjid, ctx in us.ctxs.iteritems():
                    if ctx.state == potr.context.STATE_ENCRYPTED:
                        self.us[account].getContext(fjid).disconnect()

        return PASS

    def handle_incoming_msg(self, event):
        ctx = None
        account = event.conn.name
        accjid = gajim.get_jid_from_account(account)

        if event.encrypted is not False or not event.stanza.getTag('body') \
            or not isinstance(event.stanza.getBody(), unicode) \
                or event.mtype != 'chat':
            return PASS

        try:
            ctx = self.us[account].getContext(event.fjid)
            msgtxt, tlvs = ctx.receiveMessage(event.msgtxt.encode('utf8'),
        except potr.context.NotOTRMessage, e:
            # received message was not OTR - pass it on
            return PASS
        except potr.context.UnencryptedMessage, e:
            # we are encrypted but got some plaintext
            # display it with a warning
Dicson's avatar
Dicson committed
            msgtxt = _('The following message received from %(jid)s was '
                    '*not encrypted*: [%(error)s]') % {'jid': event.fjid,
                    'error': e.args[0]}
        except potr.context.NotEncryptedError, e:
            # we got some encrypted data
            # but we don't have an encrypted session
            self.gajim_log(_('The encrypted message received from %s is '
                    'unreadable, as you are not currently communicating '
                    'privately') % event.fjid, account, event.fjid)
            return IGNORE
        except potr.context.ErrorReceived, e:
            # got a protocol error
            self.gajim_log(_('We received the following OTR error '
Dicson's avatar
Dicson committed
                    'message from %(jid)s: [%(error)s]') % {'jid': event.fjid,
Kjell Braden's avatar
Kjell Braden committed
                    'error': e.args[0].error},
                    account, event.fjid)
            return IGNORE
        except potr.crypt.InvalidParameterError, e:
            # received a packet we cannot process (probably tampered or
            # sent to wrong session)
            self.gajim_log(_('We received an unreadable OTR message '
                    'from %(jid)s. It has probably been tampered with, '
                    'or was sent from an older OTR session.')
                    % {'jid':event.fjid}, account, event.fjid)
            # generic library bug?
            self.gajim_log(_('The following error occurred when trying to '
Dicson's avatar
Dicson committed
                    'decrypt a message from %(jid)s: [%(error)s]') % {
                    'jid': event.fjid, 'error': e},

        if ctx is not None:
            ctx.smpWindow.handle_tlv(tlvs)

Kjell Braden's avatar
Kjell Braden committed
        stripper = HTMLStripper()
        stripper.feed((msgtxt or '').decode('utf8'))
Kjell Braden's avatar
Kjell Braden committed
        event.msgtxt = stripper.stripped_data
        event.stanza.setXHTML((msgtxt or '').decode('utf8'))
    def handle_outgoing_msg_stanza(self, event):
        xhtml = event.msg_iq.getXHTML()
        body = event.msg_iq.getBody()
        encrypted = False
        try:
            if xhtml:
                xhtml = xhtml.encode('utf8')
                encrypted_msg = self.us[event.conn.name].\
                    getContext(event.msg_iq.getTo()).\
                    sendMessage(potr.context.FRAGMENT_SEND_ALL_BUT_LAST, xhtml)
                if xhtml != encrypted_msg.strip(): #.strip() because sendMessage() adds whitespaces
                    encrypted = True
                    event.msg_iq.delChild('html')
                    event.msg_iq.setBody(encrypted_msg)
            elif body:
                body = escape(body).encode('utf8')
                encrypted_msg = self.us[event.conn.name].\
                    getContext(event.msg_iq.getTo()).\
                    sendMessage(potr.context.FRAGMENT_SEND_ALL_BUT_LAST, body)
                if body != encrypted_msg.strip():
                    encrypted = True
                    event.msg_iq.setBody(encrypted_msg)
        except potr.context.NotEncryptedError, e:
            if e.args[0] == potr.context.EXC_FINISHED:
                self.gajim_log(msg_not_send, event.conn.name, event.msg_iq.getTo())
                return IGNORE
            else:
                raise e

        if encrypted:
            add_message_processing_hints(event.msg_iq)

        return PASS

        try:
            if hasattr(event, 'otrmessage'):
                return PASS
            xep_200 = bool(event.session) and event.session.enable_encryption
            potrrootlog.debug('got event {0} xep_200={1}'.format(pformat(event.__dict__), xep_200))
            if xep_200 or not event.message:
                return PASS
            if event.session:
                fjid = event.session.get_to()
            else:
                fjid = event.jid
                if event.resource:
                    fjid += '/' + event.resource
            message = event.xhtml or escape(event.message)
            message = message.encode('utf8')
            potrrootlog.debug('processing message={0!r} from fjid={1!r}'.format(message, fjid))
            try:
                newmsg = self.us[event.account].getContext(fjid).sendMessage(
                        potr.context.FRAGMENT_SEND_ALL_BUT_LAST, message,
                        appdata={'session':event.session})
                potrrootlog.debug('processed message={0!r}'.format(newmsg))
            except potr.context.NotEncryptedError, e:
                if e.args[0] == potr.context.EXC_FINISHED:
                    self.gajim_log(msg_not_send, event.account, fjid)
            if event.xhtml: # if we had html before, replace with new content
                event.xhtml = newmsg
            stripper = HTMLStripper()
            stripper.feed((newmsg or '').decode('utf8'))
            event.message = stripper.stripped_data
        except:
            potrrootlog.exception('exception in outgoing message handler, message (hopefully) discarded')
            return IGNORE
Kjell Braden's avatar
Kjell Braden committed

class HTMLStripper(HTMLParser):
    def reset(self):
        self.stripped_data = ''
        HTMLParser.reset(self)

    def handle_data(self, data):
        self.stripped_data += data

    def handle_starttag(self, tag, attrs):
        if tag == 'br':
            self.stripped_data += '\n'

Kjell Braden's avatar
Kjell Braden committed
    def handle_entityref(self, name):
        try:
            c = unichr(name2codepoint[name])
        except KeyError:
            c = '&{};'.format(name)
Kjell Braden's avatar
Kjell Braden committed
        self.stripped_data += c
Kjell Braden's avatar
Kjell Braden committed
    def handle_charref(self, name):
        if name.startswith('x'):
            c = unichr(int(name[1:], 16))
        else:
            c = unichr(int(name))
        self.stripped_data += c
Kjell Braden's avatar
Kjell Braden committed
    def unknown_decl(self, data):
        if data.startswith('CDATA['):
            self.stripped_data += data[6:]

    def feed(self, data):
        data = data.replace('\n', '')
        HTMLParser.feed(self, data)
def escape(s):
    '''Replace special characters "&", "<" and ">" to HTML-safe sequences.
    If the optional flag quote is true, the quotation mark character (")
    is also translated.'''
    s = s.replace("&", "&amp;") # Must be done first!
    s = s.replace("<", "&lt;")
    s = s.replace(">", "&gt;")
    s = s.replace("\n", "<br/>")
def add_message_processing_hints(stanza):
    stanza.addChild(name='private', namespace=nbxmpp.NS_CARBONS)
    stanza.addChild(name='no-permanent-store', namespace=nbxmpp.NS_MSG_HINTS)
    stanza.addChild(name='no-copy', namespace=nbxmpp.NS_MSG_HINTS)