comparison sat/plugins/plugin_sec_otr.py @ 3028:ab2696e34d29

Python 3 port: /!\ this is a huge commit /!\ starting from this commit, SàT is needs Python 3.6+ /!\ SàT maybe be instable or some feature may not work anymore, this will improve with time This patch port backend, bridge and frontends to Python 3. Roughly this has been done this way: - 2to3 tools has been applied (with python 3.7) - all references to python2 have been replaced with python3 (notably shebangs) - fixed files not handled by 2to3 (notably the shell script) - several manual fixes - fixed issues reported by Python 3 that where not handled in Python 2 - replaced "async" with "async_" when needed (it's a reserved word from Python 3.7) - replaced zope's "implements" with @implementer decorator - temporary hack to handle data pickled in database, as str or bytes may be returned, to be checked later - fixed hash comparison for password - removed some code which is not needed anymore with Python 3 - deactivated some code which needs to be checked (notably certificate validation) - tested with jp, fixed reported issues until some basic commands worked - ported Primitivus (after porting dependencies like urwid satext) - more manual fixes
author Goffi <goffi@goffi.org>
date Tue, 13 Aug 2019 19:08:41 +0200
parents 3d735e0ab2fa
children fee60f17ebac
comparison
equal deleted inserted replaced
3027:ff5bcb12ae60 3028:ab2696e34d29
1 #!/usr/bin/env python2 1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 3
4 # SAT plugin for OTR encryption 4 # SAT plugin for OTR encryption
5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) 5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org)
6 6
19 19
20 # XXX: thanks to Darrik L Mazey for his documentation 20 # XXX: thanks to Darrik L Mazey for his documentation
21 # (https://blog.darmasoft.net/2013/06/30/using-pure-python-otr.html) 21 # (https://blog.darmasoft.net/2013/06/30/using-pure-python-otr.html)
22 # this implentation is based on it 22 # this implentation is based on it
23 23
24 import copy
25 import time
26 import uuid
27 from binascii import hexlify, unhexlify
24 from sat.core.i18n import _, D_ 28 from sat.core.i18n import _, D_
25 from sat.core.constants import Const as C 29 from sat.core.constants import Const as C
26 from sat.core.log import getLogger 30 from sat.core.log import getLogger
27 from sat.core import exceptions 31 from sat.core import exceptions
28
29 log = getLogger(__name__)
30 from sat.tools import xml_tools 32 from sat.tools import xml_tools
31 from twisted.words.protocols.jabber import jid 33 from twisted.words.protocols.jabber import jid
32 from twisted.python import failure 34 from twisted.python import failure
33 from twisted.internet import defer 35 from twisted.internet import defer
34 from sat.memory import persistent 36 from sat.memory import persistent
35 import potr 37 import potr
36 import copy 38
37 import time 39 log = getLogger(__name__)
38 import uuid
39 40
40 41
41 PLUGIN_INFO = { 42 PLUGIN_INFO = {
42 C.PI_NAME: u"OTR", 43 C.PI_NAME: "OTR",
43 C.PI_IMPORT_NAME: u"OTR", 44 C.PI_IMPORT_NAME: "OTR",
44 C.PI_TYPE: u"SEC", 45 C.PI_TYPE: "SEC",
45 C.PI_PROTOCOLS: [u"XEP-0364"], 46 C.PI_PROTOCOLS: ["XEP-0364"],
46 C.PI_DEPENDENCIES: [u"XEP-0280", u"XEP-0334"], 47 C.PI_DEPENDENCIES: ["XEP-0280", "XEP-0334"],
47 C.PI_MAIN: u"OTR", 48 C.PI_MAIN: "OTR",
48 C.PI_HANDLER: u"no", 49 C.PI_HANDLER: "no",
49 C.PI_DESCRIPTION: _(u"""Implementation of OTR"""), 50 C.PI_DESCRIPTION: _("""Implementation of OTR"""),
50 } 51 }
51 52
52 NS_OTR = "urn:xmpp:otr:0" 53 NS_OTR = "urn:xmpp:otr:0"
53 PRIVATE_KEY = "PRIVATE KEY" 54 PRIVATE_KEY = "PRIVATE KEY"
54 OTR_MENU = D_(u"OTR") 55 OTR_MENU = D_("OTR")
55 AUTH_TXT = D_( 56 AUTH_TXT = D_(
56 u"To authenticate your correspondent, you need to give your below fingerprint " 57 "To authenticate your correspondent, you need to give your below fingerprint "
57 u"*BY AN EXTERNAL CANAL* (i.e. not in this chat), and check that the one he gives " 58 "*BY AN EXTERNAL CANAL* (i.e. not in this chat), and check that the one he gives "
58 u"you is the same as below. If there is a mismatch, there can be a spy between you!" 59 "you is the same as below. If there is a mismatch, there can be a spy between you!"
59 ) 60 )
60 DROP_TXT = D_( 61 DROP_TXT = D_(
61 u"You private key is used to encrypt messages for your correspondent, nobody except " 62 "You private key is used to encrypt messages for your correspondent, nobody except "
62 u"you must know it, if you are in doubt, you should drop it!\n\nAre you sure you " 63 "you must know it, if you are in doubt, you should drop it!\n\nAre you sure you "
63 u"want to drop your private key?" 64 "want to drop your private key?"
64 ) 65 )
65 # NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and") # FIXME: not used at the moment 66 # NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and") # FIXME: not used at the moment
66 NO_ADV_FEATURES = D_(u"Some of advanced features are disabled !") 67 NO_ADV_FEATURES = D_("Some of advanced features are disabled !")
67 68
68 DEFAULT_POLICY_FLAGS = {"ALLOW_V1": False, "ALLOW_V2": True, "REQUIRE_ENCRYPTION": True} 69 DEFAULT_POLICY_FLAGS = {"ALLOW_V1": False, "ALLOW_V2": True, "REQUIRE_ENCRYPTION": True}
69 70
70 OTR_STATE_TRUSTED = "trusted" 71 OTR_STATE_TRUSTED = "trusted"
71 OTR_STATE_UNTRUSTED = "untrusted" 72 OTR_STATE_UNTRUSTED = "untrusted"
105 @param msg_str(str): encrypted message body 106 @param msg_str(str): encrypted message body
106 @param appdata(None, dict): None for signal message, 107 @param appdata(None, dict): None for signal message,
107 message data when an encrypted message is going to be sent 108 message data when an encrypted message is going to be sent
108 """ 109 """
109 assert isinstance(self.peer, jid.JID) 110 assert isinstance(self.peer, jid.JID)
110 msg = msg_str.decode("utf-8") 111 msg = msg_str
111 client = self.user.client 112 client = self.user.client
112 log.debug(u"injecting encrypted message to {to}".format(to=self.peer)) 113 log.debug("injecting encrypted message to {to}".format(to=self.peer))
113 if appdata is None: 114 if appdata is None:
114 mess_data = { 115 mess_data = {
115 "from": client.jid, 116 "from": client.jid,
116 "to": self.peer, 117 "to": self.peer,
117 "uid": unicode(uuid.uuid4()), 118 "uid": str(uuid.uuid4()),
118 "message": {"": msg}, 119 "message": {"": msg},
119 "subject": {}, 120 "subject": {},
120 "type": "chat", 121 "type": "chat",
121 "extra": {}, 122 "extra": {},
122 "timestamp": time.time(), 123 "timestamp": time.time(),
123 } 124 }
124 client.generateMessageXML(mess_data) 125 client.generateMessageXML(mess_data)
125 xml = mess_data[u'xml'] 126 xml = mess_data['xml']
126 self._p_carbons.setPrivate(xml) 127 self._p_carbons.setPrivate(xml)
127 self._p_hints.addHintElements(xml, [ 128 self._p_hints.addHintElements(xml, [
128 self._p_hints.HINT_NO_COPY, 129 self._p_hints.HINT_NO_COPY,
129 self._p_hints.HINT_NO_PERMANENT_STORE]) 130 self._p_hints.HINT_NO_PERMANENT_STORE])
130 client.send(mess_data["xml"]) 131 client.send(mess_data["xml"])
131 else: 132 else:
132 message_elt = appdata[u"xml"] 133 message_elt = appdata["xml"]
133 assert message_elt.name == u"message" 134 assert message_elt.name == "message"
134 message_elt.addElement("body", content=msg) 135 message_elt.addElement("body", content=msg)
135 136
136 def stopCb(self, __, feedback): 137 def stopCb(self, __, feedback):
137 client = self.user.client 138 client = self.user.client
138 self.host.bridge.otrState( 139 self.host.bridge.otrState(
141 client.feedback(self.peer, feedback) 142 client.feedback(self.peer, feedback)
142 143
143 def stopEb(self, failure_): 144 def stopEb(self, failure_):
144 # encryption may be already stopped in case of manual stop 145 # encryption may be already stopped in case of manual stop
145 if not failure_.check(exceptions.NotFound): 146 if not failure_.check(exceptions.NotFound):
146 log.error(u"Error while stopping OTR encryption: {msg}".format(msg=failure_)) 147 log.error("Error while stopping OTR encryption: {msg}".format(msg=failure_))
147 148
148 def isTrusted(self): 149 def isTrusted(self):
149 # we have to check value because potr code says that a 2-tuples should be 150 # we have to check value because potr code says that a 2-tuples should be
150 # returned while in practice it's either None or u"trusted" 151 # returned while in practice it's either None or u"trusted"
151 trusted = self.getCurrentTrust() 152 trusted = self.getCurrentTrust()
152 if trusted is None: 153 if trusted is None:
153 return False 154 return False
154 elif trusted == u'trusted': 155 elif trusted == 'trusted':
155 return True 156 return True
156 else: 157 else:
157 log.error(u"Unexpected getCurrentTrust() value: {value}".format( 158 log.error("Unexpected getCurrentTrust() value: {value}".format(
158 value=trusted)) 159 value=trusted))
159 return False 160 return False
160 161
161 def setState(self, state): 162 def setState(self, state):
162 client = self.user.client 163 client = self.user.client
163 old_state = self.state 164 old_state = self.state
164 super(Context, self).setState(state) 165 super(Context, self).setState(state)
165 log.debug(u"setState: %s (old_state=%s)" % (state, old_state)) 166 log.debug("setState: %s (old_state=%s)" % (state, old_state))
166 167
167 if state == potr.context.STATE_PLAINTEXT: 168 if state == potr.context.STATE_PLAINTEXT:
168 feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % { 169 feedback = _("/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {
169 "other_jid": self.peer.full() 170 "other_jid": self.peer.full()
170 } 171 }
171 d = client.encryption.stop(self.peer, NS_OTR) 172 d = client.encryption.stop(self.peer, NS_OTR)
172 d.addCallback(self.stopCb, feedback=feedback) 173 d.addCallback(self.stopCb, feedback=feedback)
173 d.addErrback(self.stopEb) 174 d.addErrback(self.stopEb)
176 client.encryption.start(self.peer, NS_OTR) 177 client.encryption.start(self.peer, NS_OTR)
177 try: 178 try:
178 trusted = self.isTrusted() 179 trusted = self.isTrusted()
179 except TypeError: 180 except TypeError:
180 trusted = False 181 trusted = False
181 trusted_str = _(u"trusted") if trusted else _(u"untrusted") 182 trusted_str = _("trusted") if trusted else _("untrusted")
182 183
183 if old_state == potr.context.STATE_ENCRYPTED: 184 if old_state == potr.context.STATE_ENCRYPTED:
184 feedback = D_( 185 feedback = D_(
185 u"{trusted} OTR conversation with {other_jid} REFRESHED" 186 "{trusted} OTR conversation with {other_jid} REFRESHED"
186 ).format(trusted=trusted_str, other_jid=self.peer.full()) 187 ).format(trusted=trusted_str, other_jid=self.peer.full())
187 else: 188 else:
188 feedback = D_( 189 feedback = D_(
189 u"{trusted} encrypted OTR conversation started with {other_jid}\n" 190 "{trusted} encrypted OTR conversation started with {other_jid}\n"
190 u"{extra_info}" 191 "{extra_info}"
191 ).format( 192 ).format(
192 trusted=trusted_str, 193 trusted=trusted_str,
193 other_jid=self.peer.full(), 194 other_jid=self.peer.full(),
194 extra_info=NO_ADV_FEATURES, 195 extra_info=NO_ADV_FEATURES,
195 ) 196 )
196 self.host.bridge.otrState( 197 self.host.bridge.otrState(
197 OTR_STATE_ENCRYPTED, self.peer.full(), client.profile 198 OTR_STATE_ENCRYPTED, self.peer.full(), client.profile
198 ) 199 )
199 elif state == potr.context.STATE_FINISHED: 200 elif state == potr.context.STATE_FINISHED:
200 feedback = D_(u"OTR conversation with {other_jid} is FINISHED").format( 201 feedback = D_("OTR conversation with {other_jid} is FINISHED").format(
201 other_jid=self.peer.full() 202 other_jid=self.peer.full()
202 ) 203 )
203 d = client.encryption.stop(self.peer, NS_OTR) 204 d = client.encryption.stop(self.peer, NS_OTR)
204 d.addCallback(self.stopCb, feedback=feedback) 205 d.addCallback(self.stopCb, feedback=feedback)
205 d.addErrback(self.stopEb) 206 d.addErrback(self.stopEb)
206 return 207 return
207 else: 208 else:
208 log.error(D_(u"Unknown OTR state")) 209 log.error(D_("Unknown OTR state"))
209 return 210 return
210 211
211 client.feedback(self.peer, feedback) 212 client.feedback(self.peer, feedback)
212 213
213 def disconnect(self): 214 def disconnect(self):
229 # we have no way to remove it from database yet (same thing for a 230 # we have no way to remove it from database yet (same thing for a
230 # correspondent jid) 231 # correspondent jid)
231 # TODO: manage explicit message encryption 232 # TODO: manage explicit message encryption
232 233
233 def __init__(self, host, client): 234 def __init__(self, host, client):
234 log.debug(u"new account: %s" % client.jid) 235 log.debug("new account: %s" % client.jid)
235 if not client.jid.resource: 236 if not client.jid.resource:
236 log.warning("Account created without resource") 237 log.warning("Account created without resource")
237 super(Account, self).__init__(unicode(client.jid), "xmpp", 1024) 238 super(Account, self).__init__(str(client.jid), "xmpp", 1024)
238 self.host = host 239 self.host = host
239 self.client = client 240 self.client = client
240 241
241 def loadPrivkey(self): 242 def loadPrivkey(self):
242 log.debug(u"loadPrivkey") 243 log.debug("loadPrivkey")
243 return self.privkey 244 return self.privkey
244 245
245 def savePrivkey(self): 246 def savePrivkey(self):
246 log.debug(u"savePrivkey") 247 log.debug("savePrivkey")
247 if self.privkey is None: 248 if self.privkey is None:
248 raise exceptions.InternalError(_(u"Save is called but privkey is None !")) 249 raise exceptions.InternalError(_("Save is called but privkey is None !"))
249 priv_key = self.privkey.serializePrivateKey().encode("hex") 250 priv_key = hexlify(self.privkey.serializePrivateKey())
250 d = self.host.memory.encryptValue(priv_key, self.client.profile) 251 d = self.host.memory.encryptValue(priv_key, self.client.profile)
251 252
252 def save_encrypted_key(encrypted_priv_key): 253 def save_encrypted_key(encrypted_priv_key):
253 self.client._otr_data[PRIVATE_KEY] = encrypted_priv_key 254 self.client._otr_data[PRIVATE_KEY] = encrypted_priv_key
254 255
255 d.addCallback(save_encrypted_key) 256 d.addCallback(save_encrypted_key)
256 257
257 def loadTrusts(self): 258 def loadTrusts(self):
258 trust_data = self.client._otr_data.get("trust", {}) 259 trust_data = self.client._otr_data.get("trust", {})
259 for jid_, jid_data in trust_data.iteritems(): 260 for jid_, jid_data in trust_data.items():
260 for fingerprint, trust_level in jid_data.iteritems(): 261 for fingerprint, trust_level in jid_data.items():
261 log.debug( 262 log.debug(
262 u'setting trust for {jid}: [{fingerprint}] = "{trust_level}"'.format( 263 'setting trust for {jid}: [{fingerprint}] = "{trust_level}"'.format(
263 jid=jid_, fingerprint=fingerprint, trust_level=trust_level 264 jid=jid_, fingerprint=fingerprint, trust_level=trust_level
264 ) 265 )
265 ) 266 )
266 self.trusts.setdefault(jid.JID(jid_), {})[fingerprint] = trust_level 267 self.trusts.setdefault(jid.JID(jid_), {})[fingerprint] = trust_level
267 268
268 def saveTrusts(self): 269 def saveTrusts(self):
269 log.debug(u"saving trusts for {profile}".format(profile=self.client.profile)) 270 log.debug("saving trusts for {profile}".format(profile=self.client.profile))
270 log.debug(u"trusts = {}".format(self.client._otr_data["trust"])) 271 log.debug("trusts = {}".format(self.client._otr_data["trust"]))
271 self.client._otr_data.force("trust") 272 self.client._otr_data.force("trust")
272 273
273 def setTrust(self, other_jid, fingerprint, trustLevel): 274 def setTrust(self, other_jid, fingerprint, trustLevel):
274 try: 275 try:
275 trust_data = self.client._otr_data["trust"] 276 trust_data = self.client._otr_data["trust"]
297 other_jid, Context(self, other_jid) 298 other_jid, Context(self, other_jid)
298 ) 299 )
299 return context 300 return context
300 301
301 def getContextForUser(self, other): 302 def getContextForUser(self, other):
302 log.debug(u"getContextForUser [%s]" % other) 303 log.debug("getContextForUser [%s]" % other)
303 if not other.resource: 304 if not other.resource:
304 log.warning(u"getContextForUser called with a bare jid: %s" % other.full()) 305 log.warning("getContextForUser called with a bare jid: %s" % other.full())
305 return self.startContext(other) 306 return self.startContext(other)
306 307
307 308
308 class OTR(object): 309 class OTR(object):
309 310
310 def __init__(self, host): 311 def __init__(self, host):
311 log.info(_(u"OTR plugin initialization")) 312 log.info(_("OTR plugin initialization"))
312 self.host = host 313 self.host = host
313 self.context_managers = {} 314 self.context_managers = {}
314 self.skipped_profiles = ( 315 self.skipped_profiles = (
315 set() 316 set()
316 ) #  FIXME: OTR should not be skipped per profile, this need to be refactored 317 ) #  FIXME: OTR should not be skipped per profile, this need to be refactored
317 self._p_hints = host.plugins[u"XEP-0334"] 318 self._p_hints = host.plugins["XEP-0334"]
318 self._p_carbons = host.plugins[u"XEP-0280"] 319 self._p_carbons = host.plugins["XEP-0280"]
319 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) 320 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
320 host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000) 321 host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000)
321 host.trigger.add("sendMessageData", self._sendMessageDataTrigger) 322 host.trigger.add("sendMessageData", self._sendMessageDataTrigger)
322 host.bridge.addMethod( 323 host.bridge.addMethod(
323 "skipOTR", ".plugin", in_sign="s", out_sign="", method=self._skipOTR 324 "skipOTR", ".plugin", in_sign="s", out_sign="", method=self._skipOTR
353 # self._dropPrivKey, 354 # self._dropPrivKey,
354 # security_limit=0, 355 # security_limit=0,
355 # type_=C.MENU_SINGLE, 356 # type_=C.MENU_SINGLE,
356 # ) 357 # )
357 host.trigger.add("presence_received", self._presenceReceivedTrigger) 358 host.trigger.add("presence_received", self._presenceReceivedTrigger)
358 self.host.registerEncryptionPlugin(self, u"OTR", NS_OTR, directed=True) 359 self.host.registerEncryptionPlugin(self, "OTR", NS_OTR, directed=True)
359 360
360 def _skipOTR(self, profile): 361 def _skipOTR(self, profile):
361 """Tell the backend to not handle OTR for this profile. 362 """Tell the backend to not handle OTR for this profile.
362 363
363 @param profile (str): %(doc_profile)s 364 @param profile (str): %(doc_profile)s
378 if encrypted_priv_key is not None: 379 if encrypted_priv_key is not None:
379 priv_key = yield self.host.memory.decryptValue( 380 priv_key = yield self.host.memory.decryptValue(
380 encrypted_priv_key, client.profile 381 encrypted_priv_key, client.profile
381 ) 382 )
382 ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey( 383 ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey(
383 priv_key.decode("hex") 384 unhexlify(priv_key.encode('utf-8'))
384 )[0] 385 )[0]
385 else: 386 else:
386 ctxMng.account.privkey = None 387 ctxMng.account.privkey = None
387 ctxMng.account.loadTrusts() 388 ctxMng.account.loadTrusts()
388 389
389 def profileDisconnected(self, client): 390 def profileDisconnected(self, client):
390 if client.profile in self.skipped_profiles: 391 if client.profile in self.skipped_profiles:
391 self.skipped_profiles.remove(client.profile) 392 self.skipped_profiles.remove(client.profile)
392 return 393 return
393 for context in client._otr_context_manager.contexts.values(): 394 for context in list(client._otr_context_manager.contexts.values()):
394 context.disconnect() 395 context.disconnect()
395 del client._otr_context_manager 396 del client._otr_context_manager
396 397
397 # encryption plugin methods 398 # encryption plugin methods
398 399
417 dialog = xml_tools.XMLUI( 418 dialog = xml_tools.XMLUI(
418 C.XMLUI_DIALOG, 419 C.XMLUI_DIALOG,
419 dialog_opt={ 420 dialog_opt={
420 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, 421 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
421 C.XMLUI_DATA_MESS: _( 422 C.XMLUI_DATA_MESS: _(
422 u"You have no private key yet, start an OTR conversation to " 423 "You have no private key yet, start an OTR conversation to "
423 u"have one" 424 "have one"
424 ), 425 ),
425 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING, 426 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING,
426 }, 427 },
427 title=_(u"No private key"), 428 title=_("No private key"),
428 ) 429 )
429 return dialog 430 return dialog
430 431
431 other_fingerprint = otrctx.getCurrentKey() 432 other_fingerprint = otrctx.getCurrentKey()
432 433
435 dialog = xml_tools.XMLUI( 436 dialog = xml_tools.XMLUI(
436 C.XMLUI_DIALOG, 437 C.XMLUI_DIALOG,
437 dialog_opt={ 438 dialog_opt={
438 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE, 439 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
439 C.XMLUI_DATA_MESS: _( 440 C.XMLUI_DATA_MESS: _(
440 u"Your fingerprint is:\n{fingerprint}\n\n" 441 "Your fingerprint is:\n{fingerprint}\n\n"
441 u"Start an OTR conversation to have your correspondent one." 442 "Start an OTR conversation to have your correspondent one."
442 ).format(fingerprint=priv_key), 443 ).format(fingerprint=priv_key),
443 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO, 444 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO,
444 }, 445 },
445 title=_(u"Fingerprint"), 446 title=_("Fingerprint"),
446 ) 447 )
447 return dialog 448 return dialog
448 449
449 def setTrust(raw_data, profile): 450 def setTrust(raw_data, profile):
450 if xml_tools.isXMLUICancelled(raw_data): 451 if xml_tools.isXMLUICancelled(raw_data):
451 return {} 452 return {}
452 # This method is called when authentication form is submited 453 # This method is called when authentication form is submited
453 data = xml_tools.XMLUIResult2DataFormResult(raw_data) 454 data = xml_tools.XMLUIResult2DataFormResult(raw_data)
454 if data["match"] == "yes": 455 if data["match"] == "yes":
455 otrctx.setCurrentTrust(OTR_STATE_TRUSTED) 456 otrctx.setCurrentTrust(OTR_STATE_TRUSTED)
456 note_msg = _(u"Your correspondent {correspondent} is now TRUSTED") 457 note_msg = _("Your correspondent {correspondent} is now TRUSTED")
457 self.host.bridge.otrState( 458 self.host.bridge.otrState(
458 OTR_STATE_TRUSTED, entity_jid.full(), client.profile 459 OTR_STATE_TRUSTED, entity_jid.full(), client.profile
459 ) 460 )
460 else: 461 else:
461 otrctx.setCurrentTrust("") 462 otrctx.setCurrentTrust("")
462 note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED") 463 note_msg = _("Your correspondent {correspondent} is now UNTRUSTED")
463 self.host.bridge.otrState( 464 self.host.bridge.otrState(
464 OTR_STATE_UNTRUSTED, entity_jid.full(), client.profile 465 OTR_STATE_UNTRUSTED, entity_jid.full(), client.profile
465 ) 466 )
466 note = xml_tools.XMLUI( 467 note = xml_tools.XMLUI(
467 C.XMLUI_DIALOG, 468 C.XMLUI_DIALOG,
475 submit_id = self.host.registerCallback(setTrust, with_data=True, one_shot=True) 476 submit_id = self.host.registerCallback(setTrust, with_data=True, one_shot=True)
476 trusted = otrctx.isTrusted() 477 trusted = otrctx.isTrusted()
477 478
478 xmlui = xml_tools.XMLUI( 479 xmlui = xml_tools.XMLUI(
479 C.XMLUI_FORM, 480 C.XMLUI_FORM,
480 title=_(u"Authentication ({entity_jid})").format(entity_jid=entity_jid.full()), 481 title=_("Authentication ({entity_jid})").format(entity_jid=entity_jid.full()),
481 submit_id=submit_id, 482 submit_id=submit_id,
482 ) 483 )
483 xmlui.addText(_(AUTH_TXT)) 484 xmlui.addText(_(AUTH_TXT))
484 xmlui.addDivider() 485 xmlui.addDivider()
485 xmlui.addText( 486 xmlui.addText(
486 D_(u"Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key) 487 D_("Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key)
487 ) 488 )
488 xmlui.addText( 489 xmlui.addText(
489 D_(u"Your correspondent fingerprint should be:\n{fingerprint}").format( 490 D_("Your correspondent fingerprint should be:\n{fingerprint}").format(
490 fingerprint=other_fingerprint 491 fingerprint=other_fingerprint
491 ) 492 )
492 ) 493 )
493 xmlui.addDivider("blank") 494 xmlui.addDivider("blank")
494 xmlui.changeContainer("pairs") 495 xmlui.changeContainer("pairs")
495 xmlui.addLabel(D_(u"Is your correspondent fingerprint the same as here ?")) 496 xmlui.addLabel(D_("Is your correspondent fingerprint the same as here ?"))
496 xmlui.addList( 497 xmlui.addList(
497 "match", [("yes", _("yes")), ("no", _("no"))], ["yes" if trusted else "no"] 498 "match", [("yes", _("yes")), ("no", _("no"))], ["yes" if trusted else "no"]
498 ) 499 )
499 return xmlui 500 return xmlui
500 501
506 """ 507 """
507 client = self.host.getClient(profile) 508 client = self.host.getClient(profile)
508 try: 509 try:
509 to_jid = jid.JID(menu_data["jid"]) 510 to_jid = jid.JID(menu_data["jid"])
510 except KeyError: 511 except KeyError:
511 log.error(_(u"jid key is not present !")) 512 log.error(_("jid key is not present !"))
512 return defer.fail(exceptions.DataError) 513 return defer.fail(exceptions.DataError)
513 self.startRefresh(client, to_jid) 514 self.startRefresh(client, to_jid)
514 return {} 515 return {}
515 516
516 def startRefresh(self, client, to_jid): 517 def startRefresh(self, client, to_jid):
517 """Start or refresh an OTR session 518 """Start or refresh an OTR session
518 519
519 @param to_jid(jid.JID): jid to start encrypted session with 520 @param to_jid(jid.JID): jid to start encrypted session with
520 """ 521 """
521 encrypted_session = client.encryption.getSession(to_jid.userhostJID()) 522 encrypted_session = client.encryption.getSession(to_jid.userhostJID())
522 if encrypted_session and encrypted_session[u'plugin'].namespace != NS_OTR: 523 if encrypted_session and encrypted_session['plugin'].namespace != NS_OTR:
523 raise exceptions.ConflictError(_( 524 raise exceptions.ConflictError(_(
524 u"Can't start an OTR session, there is already an encrypted session " 525 "Can't start an OTR session, there is already an encrypted session "
525 u"with {name}").format(name=encrypted_session[u'plugin'].name)) 526 "with {name}").format(name=encrypted_session['plugin'].name))
526 if not to_jid.resource: 527 if not to_jid.resource:
527 to_jid.resource = self.host.memory.getMainResource( 528 to_jid.resource = self.host.memory.getMainResource(
528 client, to_jid 529 client, to_jid
529 ) # FIXME: temporary and unsecure, must be changed when frontends 530 ) # FIXME: temporary and unsecure, must be changed when frontends
530 # are refactored 531 # are refactored
540 """ 541 """
541 client = self.host.getClient(profile) 542 client = self.host.getClient(profile)
542 try: 543 try:
543 to_jid = jid.JID(menu_data["jid"]) 544 to_jid = jid.JID(menu_data["jid"])
544 except KeyError: 545 except KeyError:
545 log.error(_(u"jid key is not present !")) 546 log.error(_("jid key is not present !"))
546 return defer.fail(exceptions.DataError) 547 return defer.fail(exceptions.DataError)
547 self.endSession(client, to_jid) 548 self.endSession(client, to_jid)
548 return {} 549 return {}
549 550
550 def endSession(self, client, to_jid): 551 def endSession(self, client, to_jid):
566 """ 567 """
567 client = self.host.getClient(profile) 568 client = self.host.getClient(profile)
568 try: 569 try:
569 to_jid = jid.JID(menu_data["jid"]) 570 to_jid = jid.JID(menu_data["jid"])
570 except KeyError: 571 except KeyError:
571 log.error(_(u"jid key is not present !")) 572 log.error(_("jid key is not present !"))
572 return defer.fail(exceptions.DataError) 573 return defer.fail(exceptions.DataError)
573 return self.authenticate(client, to_jid) 574 return self.authenticate(client, to_jid)
574 575
575 def authenticate(self, client, to_jid): 576 def authenticate(self, client, to_jid):
576 """Authenticate other user and see our own fingerprint""" 577 """Authenticate other user and see our own fingerprint"""
590 to_jid.resource = self.host.memory.getMainResource( 591 to_jid.resource = self.host.memory.getMainResource(
591 client, to_jid 592 client, to_jid
592 ) # FIXME: temporary and unsecure, must be changed when frontends 593 ) # FIXME: temporary and unsecure, must be changed when frontends
593 # are refactored 594 # are refactored
594 except KeyError: 595 except KeyError:
595 log.error(_(u"jid key is not present !")) 596 log.error(_("jid key is not present !"))
596 return defer.fail(exceptions.DataError) 597 return defer.fail(exceptions.DataError)
597 598
598 ctxMng = client._otr_context_manager 599 ctxMng = client._otr_context_manager
599 if ctxMng.account.privkey is None: 600 if ctxMng.account.privkey is None:
600 return { 601 return {
601 "xmlui": xml_tools.note(_(u"You don't have a private key yet !")).toXml() 602 "xmlui": xml_tools.note(_("You don't have a private key yet !")).toXml()
602 } 603 }
603 604
604 def dropKey(data, profile): 605 def dropKey(data, profile):
605 if C.bool(data["answer"]): 606 if C.bool(data["answer"]):
606 # we end all sessions 607 # we end all sessions
607 for context in ctxMng.contexts.values(): 608 for context in list(ctxMng.contexts.values()):
608 context.disconnect() 609 context.disconnect()
609 ctxMng.account.privkey = None 610 ctxMng.account.privkey = None
610 ctxMng.account.getPrivkey() # as account.privkey is None, getPrivkey 611 ctxMng.account.getPrivkey() # as account.privkey is None, getPrivkey
611 # will generate a new key, and save it 612 # will generate a new key, and save it
612 return { 613 return {
613 "xmlui": xml_tools.note( 614 "xmlui": xml_tools.note(
614 D_(u"Your private key has been dropped") 615 D_("Your private key has been dropped")
615 ).toXml() 616 ).toXml()
616 } 617 }
617 return {} 618 return {}
618 619
619 submit_id = self.host.registerCallback(dropKey, with_data=True, one_shot=True) 620 submit_id = self.host.registerCallback(dropKey, with_data=True, one_shot=True)
620 621
621 confirm = xml_tools.XMLUI( 622 confirm = xml_tools.XMLUI(
622 C.XMLUI_DIALOG, 623 C.XMLUI_DIALOG,
623 title=_(u"Confirm private key drop"), 624 title=_("Confirm private key drop"),
624 dialog_opt={"type": C.XMLUI_DIALOG_CONFIRM, "message": _(DROP_TXT)}, 625 dialog_opt={"type": C.XMLUI_DIALOG_CONFIRM, "message": _(DROP_TXT)},
625 submit_id=submit_id, 626 submit_id=submit_id,
626 ) 627 )
627 return {"xmlui": confirm.toXml()} 628 return {"xmlui": confirm.toXml()}
628 629
629 def _receivedTreatment(self, data, client): 630 def _receivedTreatment(self, data, client):
630 from_jid = data["from"] 631 from_jid = data["from"]
631 log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid) 632 log.debug("_receivedTreatment [from_jid = %s]" % from_jid)
632 otrctx = client._otr_context_manager.getContextForUser(from_jid) 633 otrctx = client._otr_context_manager.getContextForUser(from_jid)
633 634
634 try: 635 try:
635 message = ( 636 message = (
636 data["message"].itervalues().next() 637 next(iter(data["message"].values()))
637 ) # FIXME: Q&D fix for message refactoring, message is now a dict 638 ) # FIXME: Q&D fix for message refactoring, message is now a dict
638 res = otrctx.receiveMessage(message.encode("utf-8")) 639 res = otrctx.receiveMessage(message.encode("utf-8"))
639 except potr.context.UnencryptedMessage: 640 except potr.context.UnencryptedMessage:
640 encrypted = False 641 encrypted = False
641 if otrctx.state == potr.context.STATE_ENCRYPTED: 642 if otrctx.state == potr.context.STATE_ENCRYPTED:
642 log.warning( 643 log.warning(
643 u"Received unencrypted message in an encrypted context (from {jid})" 644 "Received unencrypted message in an encrypted context (from {jid})"
644 .format(jid=from_jid.full()) 645 .format(jid=from_jid.full())
645 ) 646 )
646 647
647 feedback = ( 648 feedback = (
648 D_( 649 D_(
649 u"WARNING: received unencrypted data in a supposedly encrypted " 650 "WARNING: received unencrypted data in a supposedly encrypted "
650 u"context" 651 "context"
651 ), 652 ),
652 ) 653 )
653 client.feedback(from_jid, feedback) 654 client.feedback(from_jid, feedback)
654 except potr.context.NotEncryptedError: 655 except potr.context.NotEncryptedError:
655 msg = D_(u"WARNING: received OTR encrypted data in an unencrypted context") 656 msg = D_("WARNING: received OTR encrypted data in an unencrypted context")
656 log.warning(msg) 657 log.warning(msg)
657 feedback = msg 658 feedback = msg
658 client.feedback(from_jid, msg) 659 client.feedback(from_jid, msg)
659 raise failure.Failure(exceptions.CancelError(msg)) 660 raise failure.Failure(exceptions.CancelError(msg))
660 except potr.context.ErrorReceived as e: 661 except potr.context.ErrorReceived as e:
661 msg = D_(u"WARNING: received OTR error message: {msg}".format(msg=e)) 662 msg = D_("WARNING: received OTR error message: {msg}".format(msg=e))
662 log.warning(msg) 663 log.warning(msg)
663 feedback = msg 664 feedback = msg
664 client.feedback(from_jid, msg) 665 client.feedback(from_jid, msg)
665 raise failure.Failure(exceptions.CancelError(msg)) 666 raise failure.Failure(exceptions.CancelError(msg))
666 except potr.crypt.InvalidParameterError as e: 667 except potr.crypt.InvalidParameterError as e:
667 msg = D_(u"Error while trying de decrypt OTR message: {msg}".format(msg=e)) 668 msg = D_("Error while trying de decrypt OTR message: {msg}".format(msg=e))
668 log.warning(msg) 669 log.warning(msg)
669 feedback = msg 670 feedback = msg
670 client.feedback(from_jid, msg) 671 client.feedback(from_jid, msg)
671 raise failure.Failure(exceptions.CancelError(msg)) 672 raise failure.Failure(exceptions.CancelError(msg))
672 except StopIteration: 673 except StopIteration:
678 if res[0] != None: 679 if res[0] != None:
679 # decrypted messages handling. 680 # decrypted messages handling.
680 # receiveMessage() will return a tuple, 681 # receiveMessage() will return a tuple,
681 # the first part of which will be the decrypted message 682 # the first part of which will be the decrypted message
682 data["message"] = { 683 data["message"] = {
683 "": res[0].decode("utf-8") 684 "": res[0]
684 } # FIXME: Q&D fix for message refactoring, message is now a dict 685 } # FIXME: Q&D fix for message refactoring, message is now a dict
685 try: 686 try:
686 # we want to keep message in history, even if no store is 687 # we want to keep message in history, even if no store is
687 # requested in message hints 688 # requested in message hints
688 del data[u"history"] 689 del data["history"]
689 except KeyError: 690 except KeyError:
690 pass 691 pass
691 # TODO: add skip history as an option, but by default we don't skip it 692 # TODO: add skip history as an option, but by default we don't skip it
692 # data[u'history'] = C.HISTORY_SKIP # we send the decrypted message to 693 # data[u'history'] = C.HISTORY_SKIP # we send the decrypted message to
693 # frontends, but we don't want it in 694 # frontends, but we don't want it in
713 but we still need to check if the message must be stored in history or not 714 but we still need to check if the message must be stored in history or not
714 """ 715 """
715 #  XXX: FIXME: this should not be done on a per-profile basis, but per-message 716 #  XXX: FIXME: this should not be done on a per-profile basis, but per-message
716 try: 717 try:
717 message = ( 718 message = (
718 data["message"].itervalues().next().encode("utf-8") 719 iter(data["message"].values()).next().encode("utf-8")
719 ) # FIXME: Q&D fix for message refactoring, message is now a dict 720 ) # FIXME: Q&D fix for message refactoring, message is now a dict
720 except StopIteration: 721 except StopIteration:
721 return data 722 return data
722 if message.startswith(potr.proto.OTRTAG): 723 if message.startswith(potr.proto.OTRTAG):
723 #  FIXME: it may be better to cancel the message and send it direclty to 724 #  FIXME: it may be better to cancel the message and send it direclty to
724 # bridge 725 # bridge
725 # this is used by Libervia, but this may send garbage message to 726 # this is used by Libervia, but this may send garbage message to
726 # other frontends 727 # other frontends
727 # if they are used at the same time as Libervia. 728 # if they are used at the same time as Libervia.
728 # Hard to avoid with decryption on Libervia though. 729 # Hard to avoid with decryption on Libervia though.
729 data[u"history"] = C.HISTORY_SKIP 730 data["history"] = C.HISTORY_SKIP
730 return data 731 return data
731 732
732 def MessageReceivedTrigger(self, client, message_elt, post_treat): 733 def MessageReceivedTrigger(self, client, message_elt, post_treat):
733 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT: 734 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
734 # OTR is not possible in group chats 735 # OTR is not possible in group chats
753 client, to_jid 754 client, to_jid
754 ) # FIXME: temporary and unsecure, must be changed when frontends 755 ) # FIXME: temporary and unsecure, must be changed when frontends
755 otrctx = client._otr_context_manager.getContextForUser(to_jid) 756 otrctx = client._otr_context_manager.getContextForUser(to_jid)
756 message_elt = mess_data["xml"] 757 message_elt = mess_data["xml"]
757 if otrctx.state == potr.context.STATE_ENCRYPTED: 758 if otrctx.state == potr.context.STATE_ENCRYPTED:
758 log.debug(u"encrypting message") 759 log.debug("encrypting message")
759 body = None 760 body = None
760 for child in list(message_elt.children): 761 for child in list(message_elt.children):
761 if child.name == "body": 762 if child.name == "body":
762 # we remove all unencrypted body, 763 # we remove all unencrypted body,
763 # and will only encrypt the first one 764 # and will only encrypt the first one
766 message_elt.children.remove(child) 767 message_elt.children.remove(child)
767 elif child.name == "html": 768 elif child.name == "html":
768 # we don't want any XHTML-IM element 769 # we don't want any XHTML-IM element
769 message_elt.children.remove(child) 770 message_elt.children.remove(child)
770 if body is None: 771 if body is None:
771 log.warning(u"No message found") 772 log.warning("No message found")
772 else: 773 else:
773 self._p_carbons.setPrivate(message_elt) 774 self._p_carbons.setPrivate(message_elt)
774 self._p_hints.addHintElements(message_elt, [ 775 self._p_hints.addHintElements(message_elt, [
775 self._p_hints.HINT_NO_COPY, 776 self._p_hints.HINT_NO_COPY,
776 self._p_hints.HINT_NO_PERMANENT_STORE]) 777 self._p_hints.HINT_NO_PERMANENT_STORE])
777 otrctx.sendMessage(0, unicode(body).encode("utf-8"), appdata=mess_data) 778 otrctx.sendMessage(0, str(body).encode("utf-8"), appdata=mess_data)
778 else: 779 else:
779 feedback = D_( 780 feedback = D_(
780 u"Your message was not sent because your correspondent closed the " 781 "Your message was not sent because your correspondent closed the "
781 u"encrypted conversation on his/her side. " 782 "encrypted conversation on his/her side. "
782 u"Either close your own side, or refresh the session." 783 "Either close your own side, or refresh the session."
783 ) 784 )
784 log.warning(_(u"Message discarded because closed encryption channel")) 785 log.warning(_("Message discarded because closed encryption channel"))
785 client.feedback(to_jid, feedback) 786 client.feedback(to_jid, feedback)
786 raise failure.Failure(exceptions.CancelError(u"Cancelled by OTR plugin")) 787 raise failure.Failure(exceptions.CancelError("Cancelled by OTR plugin"))
787 788
788 def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, 789 def sendMessageTrigger(self, client, mess_data, pre_xml_treatments,
789 post_xml_treatments): 790 post_xml_treatments):
790 if mess_data["type"] == "groupchat": 791 if mess_data["type"] == "groupchat":
791 return True 792 return True