comparison browser/sat_browser/plugin_sec_otr.py @ 1124:28e3eb3bb217

files reorganisation and installation rework: - files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory) - VERSION file is now used, as for other SàT projects - replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly - removed check for data_dir if it's empty - installation tested working in virtual env - libervia launching script is now in bin/libervia
author Goffi <goffi@goffi.org>
date Sat, 25 Aug 2018 17:59:48 +0200
parents src/browser/sat_browser/plugin_sec_otr.py@f2170536ba23
children 2af117bfe6cc
comparison
equal deleted inserted replaced
1123:63a4b8fe9782 1124:28e3eb3bb217
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # Libervia plugin for OTR encryption
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6 # Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org)
7
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
17
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21 """
22 This file is adapted from sat.plugins.plugin.sec_otr. It offers browser-side OTR encryption using otr.js.
23 The text messages to display are mostly taken from the Pidgin OTR plugin (GPL 2.0, see http://otr.cypherpunks.ca).
24 """
25
26 from sat.core.log import getLogger
27 log = getLogger(__name__)
28
29 from sat.core.i18n import _, D_
30 from sat.core import exceptions
31 from sat.tools import trigger
32
33 from constants import Const as C
34 from sat_frontends.tools import jid
35 import otrjs_wrapper as otr
36 import dialog
37 import chat
38 import uuid
39 import time
40
41
42 NS_OTR = "otr_plugin"
43 PRIVATE_KEY = "PRIVATE KEY"
44 MAIN_MENU = D_('OTR') # TODO: get this constant directly from backend's plugin
45 DIALOG_EOL = "<br />"
46
47 AUTH_TRUSTED = D_("Verified")
48 AUTH_UNTRUSTED = D_("Unverified")
49 AUTH_OTHER_TITLE = D_("Authentication of {jid}")
50 AUTH_US_TITLE = D_("Authentication to {jid}")
51 AUTH_TRUST_NA_TITLE = D_("Authentication requirement")
52 AUTH_TRUST_NA_TXT = D_("You must start an OTR conversation before authenticating your correspondent.")
53 AUTH_INFO_TXT = D_("Authenticating a correspondent helps ensure that the person you are talking to is who he or she claims to be.{eol}{eol}")
54 AUTH_FINGERPRINT_YOURS = D_("Your fingerprint is:{eol}{fingerprint}{eol}{eol}Start an OTR conversation to have your correspondent one.")
55 AUTH_FINGERPRINT_TXT = D_("To verify the fingerprint, contact your correspondent via some other authenticated channel (i.e. not in this chat), such as the telephone or GPG-signed email. Each of you should tell your fingerprint to the other.{eol}{eol}")
56 AUTH_FINGERPRINT_VERIFY = D_("Fingerprint for you, {you}:{eol}{your_fp}{eol}{eol}Purported fingerprint for {other}:{eol}{other_fp}{eol}{eol}Did you verify that this is in fact the correct fingerprint for {other}?")
57 AUTH_QUEST_DEFINE_TXT = D_("To authenticate using a question, pick a question whose answer is known only to you and your correspondent. Enter this question and this answer, then wait for your correspondent to enter the answer too. If the answers don't match, then you may be talking to an imposter.{eol}{eol}")
58 AUTH_QUEST_DEFINE = D_("Enter question here:{eol}")
59 AUTH_QUEST_ANSWER_TXT = D_("Your correspondent is attempting to determine if he or she is really talking to you, or if it's someone pretending to be you. Your correspondent has asked a question, indicated below. To authenticate to your correspondent, enter the answer and click OK.{eol}{eol}")
60 AUTH_QUEST_ANSWER = D_("This is the question asked by your correspondent:{eol}{question}")
61 AUTH_SECRET_INPUT = D_("{eol}{eol}Enter secret answer here: (case sensitive){eol}")
62 AUTH_ABORTED_TXT = D_("Authentication aborted.")
63 AUTH_FAILED_TXT = D_("Authentication failed.")
64 AUTH_OTHER_OK = D_("Authentication successful.")
65 AUTH_US_OK = D_("Your correspondent has successfully authenticated you.")
66 AUTH_OTHER_TOO = D_("You may want to authenticate your correspondent as well by asking your own question.")
67 AUTH_STATUS = D_("The current conversation is now {state}.")
68
69 FINISHED_CONTEXT_TITLE = D_('Finished OTR conversation with {jid}')
70 SEND_PLAIN_IN_FINISHED_CONTEXT = D_("Your message was not sent because your correspondent closed the OTR conversation on his/her side. Either close your own side, or refresh the session.")
71 RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT = D_("WARNING: received unencrypted data in a supposedly encrypted context!")
72
73 QUERY_ENCRYPTED = D_('Attempting to refresh the OTR conversation with {jid}...')
74 QUERY_NOT_ENCRYPTED = D_('Attempting to start an OTR conversation with {jid}...')
75 AKE_ENCRYPTED = D_(" conversation with {jid} started. Your client is not logging this conversation.")
76 AKE_NOT_ENCRYPTED = D_("ERROR: successfully ake'd with {jid} but the conversation is not encrypted!")
77 END_ENCRYPTED = D_("ERROR: the OTR session ended but the context is still supposedly encrypted!")
78 END_PLAIN_NO_MORE = D_("Your conversation with {jid} is no more encrypted.")
79 END_PLAIN_HAS_NOT = D_("Your conversation with {jid} hasn't been encrypted.")
80 END_FINISHED = D_("{jid} has ended his or her private conversation with you; you should do the same.")
81
82 KEY_TITLE = D_('Private key')
83 KEY_NA_TITLE = D_("No private key")
84 KEY_NA_TXT = D_("You don't have any private key yet.")
85 KEY_DROP_TITLE = D_('Drop your private key')
86 KEY_DROP_TXT = D_("You private key is used to encrypt messages for your correspondent, nobody except you must know it, if you are in doubt, you should drop it!{eol}{eol}Are you sure you want to drop your private key?")
87 KEY_DROPPED_TXT = D_("Your private key has been dropped.")
88
89 QUERY_TITLE = D_("Going encrypted")
90 QUERY_RECEIVED = D_("{jid} is willing to start with you an OTR encrypted conversation.{eol}{eol}")
91 QUERY_SEND = D_("You are about to start an OTR encrypted conversation with {jid}.{eol}{eol}")
92 QUERY_SLOWDOWN = D_("This end-to-end encryption is computed by your web browser and you may experience slowdowns.{eol}{eol}")
93 QUERY_NO_KEY = D_("This will take up to 10 seconds to generate your single use private key and start the conversation. In a future version of Libervia, your private key will be safely and persistently stored, so you will have to generate it only once.{eol}{eol}")
94 QUERY_KEY = D_("You already have a private key, but to start the conversation will still require a couple of seconds.{eol}{eol}")
95 QUERY_CONFIRM = D_("Press OK to start now the encryption.")
96
97 ACTION_NA_TITLE = D_("Impossible action")
98 ACTION_NA = D_("Your correspondent must be connected to start an OTR conversation with him.")
99
100 DEFAULT_POLICY_FLAGS = {
101 'ALLOW_V2': True,
102 'ALLOW_V3': True,
103 'REQUIRE_ENCRYPTION': False,
104 'SEND_WHITESPACE_TAG': False, # FIXME: we need to complete sendMessageTg before turning this to True
105 'WHITESPACE_START_AKE': False, # FIXME: we need to complete newMessageTg before turning this to True
106 }
107
108 # list a couple of texts or htmls (untrusted, trusted) for each state
109 OTR_MSG_STATES = {
110 otr.context.STATE_PLAINTEXT: [
111 '<img src="media/icons/silk/lock_open.png" /><img src="media/icons/silk/key_delete.png" />',
112 '<img src="media/icons/silk/lock_open.png" /><img src="media/icons/silk/key.png" />'
113 ],
114 otr.context.STATE_ENCRYPTED: [
115 '<img src="media/icons/silk/lock.png" /><img src="media/icons/silk/key_delete.png" />',
116 '<img src="media/icons/silk/lock.png" /><img src="media/icons/silk/key.png" />'
117 ],
118 otr.context.STATE_FINISHED: [
119 '<img src="media/icons/silk/lock_break.png" /><img src="media/icons/silk/key_delete.png" />',
120 '<img src="media/icons/silk/lock_break.png" /><img src="media/icons/silk/key.png" />'
121 ]
122 }
123
124
125 unicode = str # FIXME: pyjamas workaround
126
127
128 class NotConnectedEntity(Exception):
129 pass
130
131
132 class Context(otr.context.Context):
133
134 def __init__(self, host, account, other_jid):
135 """
136
137 @param host (satWebFrontend)
138 @param account (Account)
139 @param other_jid (jid.JID): JID of the person your chat correspondent
140 """
141 super(Context, self).__init__(account, other_jid)
142 self.host = host
143
144 def getPolicy(self, key):
145 """Get the value of the specified policy
146
147 @param key (unicode): a value in:
148 - ALLOW_V1 (apriori removed from otr.js)
149 - ALLOW_V2
150 - ALLOW_V3
151 - REQUIRE_ENCRYPTION
152 - SEND_WHITESPACE_TAG
153 - WHITESPACE_START_AKE
154 - ERROR_START_AKE
155 @return: unicode
156 """
157 if key in DEFAULT_POLICY_FLAGS:
158 return DEFAULT_POLICY_FLAGS[key]
159 else:
160 return False
161
162 def receiveMessageCb(self, msg, encrypted):
163 assert isinstance(self.peer, jid.JID)
164 if not encrypted:
165 log.warning(u"A plain-text message has been handled by otr.js")
166 log.debug(u"message received (was %s): %s" % ('encrypted' if encrypted else 'plain', msg))
167 uuid_ = str(uuid.uuid4()) # FIXME
168 if not encrypted:
169 if self.state == otr.context.STATE_ENCRYPTED:
170 log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer})
171 self.host.newMessageHandler(uuid_, time.time(), unicode(self.peer), unicode(self.host.whoami), {'': RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT}, {}, C.MESS_TYPE_INFO, {})
172 self.host.newMessageHandler(uuid_, time.time(), unicode(self.peer), unicode(self.host.whoami), {'': msg}, {}, C.MESS_TYPE_CHAT, {})
173
174 def sendMessageCb(self, msg, meta=None):
175 assert isinstance(self.peer, jid.JID)
176 log.debug(u"message to send%s: %s" % ((' (attached meta data: %s)' % meta) if meta else '', msg))
177 self.host.bridge.call('sendMessage', (None, self.host.sendError), unicode(self.peer), msg, '', C.MESS_TYPE_CHAT, {'send_only': 'true'})
178
179 def messageErrorCb(self, error):
180 log.error(u'error occured: %s' % error)
181
182 def setStateCb(self, msg_state, status):
183 if status == otr.context.STATUS_AKE_INIT:
184 return
185
186 other_jid_s = self.peer
187 feedback = _(u"Error: the state of the conversation with %s is unknown!")
188 trust = self.getCurrentTrust()
189
190 if status == otr.context.STATUS_SEND_QUERY:
191 feedback = QUERY_ENCRYPTED if msg_state == otr.context.STATE_ENCRYPTED else QUERY_NOT_ENCRYPTED
192
193 elif status == otr.context.STATUS_AKE_SUCCESS:
194 trusted_str = AUTH_TRUSTED if trust else AUTH_UNTRUSTED
195 feedback = (trusted_str + AKE_ENCRYPTED) if msg_state == otr.context.STATE_ENCRYPTED else AKE_NOT_ENCRYPTED
196
197 elif status == otr.context.STATUS_END_OTR:
198 if msg_state == otr.context.STATE_PLAINTEXT:
199 feedback = END_PLAIN_NO_MORE
200 elif msg_state == otr.context.STATE_ENCRYPTED:
201 log.error(END_ENCRYPTED)
202 elif msg_state == otr.context.STATE_FINISHED:
203 feedback = END_FINISHED
204
205 uuid_ = str(uuid.uuid4()) # FIXME
206 self.host.newMessageHandler(uuid_, time.time(), unicode(self.peer), unicode(self.host.whoami), {'': feedback.format(jid=other_jid_s)}, {}, C.MESS_TYPE_INFO, {'header_info': OTR.getInfoText(msg_state, trust)})
207
208 def setCurrentTrust(self, new_trust='', act='asked', type_='trust'):
209 log.debug(u"setCurrentTrust: trust={trust}, act={act}, type={type}".format(type=type_, trust=new_trust, act=act))
210 title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer)
211 old_trust = self.getCurrentTrust()
212 if type_ == 'abort':
213 msg = AUTH_ABORTED_TXT
214 elif new_trust:
215 if act == "asked":
216 msg = AUTH_OTHER_OK
217 else:
218 msg = AUTH_US_OK
219 if not old_trust:
220 msg += " " + AUTH_OTHER_TOO
221 else:
222 msg = AUTH_FAILED_TXT
223 dialog.InfoDialog(title, msg, AddStyleName="maxWidthLimit").show()
224 if act != "asked":
225 return
226 otr.context.Context.setCurrentTrust(self, new_trust)
227 if old_trust != new_trust:
228 feedback = AUTH_STATUS.format(state=(AUTH_TRUSTED if new_trust else AUTH_UNTRUSTED).lower())
229 uuid_ = str(uuid.uuid4()) # FIXME
230 self.host.newMessageHandler(uuid_, time.time(), unicode(self.peer), unicode(self.host.whoami), {'': feedback}, {}, C.MESS_TYPE_INFO, {'header_info': OTR.getInfoText(self.state, new_trust)})
231
232 def fingerprintAuthCb(self):
233 """OTR v2 authentication using manual fingerprint comparison"""
234 priv_key = self.user.privkey
235
236 if priv_key is None: # OTR._authenticate should not let us arrive here
237 raise exceptions.InternalError
238 return
239
240 other_key = self.getCurrentKey()
241 if other_key is None:
242 # we have a private key, but not the fingerprint of our correspondent
243 msg = (AUTH_INFO_TXT + AUTH_FINGERPRINT_YOURS).format(fingerprint=priv_key.fingerprint(), eol=DIALOG_EOL)
244 dialog.InfoDialog(_("Fingerprint"), msg, AddStyleName="maxWidthLimit").show()
245 return
246
247 def setTrust(confirm):
248 self.setCurrentTrust('fingerprint' if confirm else '')
249
250 text = (AUTH_INFO_TXT + "<i>" + AUTH_FINGERPRINT_TXT + "</i>" + AUTH_FINGERPRINT_VERIFY).format(you=self.host.whoami, your_fp=priv_key.fingerprint(), other=self.peer, other_fp=other_key.fingerprint(), eol=DIALOG_EOL)
251 title = AUTH_OTHER_TITLE.format(jid=self.peer)
252 dialog.ConfirmDialog(setTrust, text, title, AddStyleName="maxWidthLimit").show()
253
254 def smpAuthCb(self, type_, data, act=None):
255 """OTR v3 authentication using the socialist millionaire protocol.
256
257 @param type_ (unicode): a value in ('question', 'trust', 'abort')
258 @param data (unicode, bool): this could be:
259 - a string containing the question if type_ is 'question'
260 - a boolean value telling if the authentication succeed when type_ is 'trust'
261 @param act (unicode): a value in ('asked', 'answered')
262 """
263 log.debug(u"smpAuthCb: type={type}, data={data}, act={act}".format(type=type_, data=data, act=act))
264 if act is None:
265 if type_ == 'question':
266 act = 'answered' # OTR._authenticate calls this method with act="asked"
267 elif type_ == 'abort':
268 act = 'asked' # smpAuthAbort triggers this method with act='answered' when needed
269
270 # FIXME upstream: if the correspondent uses Pidgin and authenticate us via
271 # fingerprints, we will reach this code... that's wrong, this method is for SMP!
272 # There's probably a bug to fix in otr.js. Do it together with the issue that
273 # make us need the dirty self.smpAuthAbort.
274 else:
275 log.error("FIXME: unmanaged ambiguous 'act' value in Context.smpAuthCb!")
276 title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer)
277 if type_ == 'question':
278 if act == 'asked':
279 def cb(result, question, answer=None):
280 if not result or not answer: # dialog cancelled or the answer is empty
281 return
282 self.smpAuthSecret(answer, question)
283 text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_DEFINE_TXT + "</i>" + AUTH_QUEST_DEFINE).format(eol=DIALOG_EOL)
284 dialog.PromptDialog(cb, [text, AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show()
285 else:
286 def cb(result, answer):
287 if not result or not answer: # dialog cancelled or the answer is empty
288 self.smpAuthAbort('answered')
289 return
290 self.smpAuthSecret(answer)
291 text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_ANSWER_TXT + "</i>" + AUTH_QUEST_ANSWER).format(eol=DIALOG_EOL, question=data)
292 dialog.PromptDialog(cb, [text + AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show()
293 elif type_ == 'trust':
294 self.setCurrentTrust('smp' if data else '', act)
295 elif type_ == 'abort':
296 self.setCurrentTrust('', act, 'abort')
297
298 def disconnect(self):
299 """Disconnect the session."""
300 if self.state != otr.context.STATE_PLAINTEXT:
301 super(Context, self).disconnect()
302
303 def finish(self):
304 """Finish the session - avoid to send any message but the user still has to end the session himself."""
305 if self.state == otr.context.STATE_ENCRYPTED:
306 super(Context, self).finish()
307
308
309 class Account(otr.context.Account):
310
311 def __init__(self, host):
312 log.debug(u"new account: %s" % host.whoami)
313 if not host.whoami.resource:
314 log.warning("Account created without resource")
315 super(Account, self).__init__(host.whoami)
316 self.host = host
317
318 def loadPrivkey(self):
319 return self.privkey
320
321 def savePrivkey(self):
322 # TODO: serialize and encrypt the private key and save it to a HTML5 persistent storage
323 # We need to ask the user before saving the key (e.g. if he's not on his private machine)
324 # self.privkey.serializePrivateKey() --> encrypt --> store
325 if self.privkey is None:
326 raise exceptions.InternalError(_("Save is called but privkey is None !"))
327 pass
328
329 def saveTrusts(self):
330 # TODO save the trusts as it would be done for the private key
331 pass
332
333
334 class ContextManager(object):
335
336 def __init__(self, host):
337 self.host = host
338 self.account = Account(host)
339 self.contexts = {}
340
341 def startContext(self, other_jid):
342 assert isinstance(other_jid, jid.JID) # never start an OTR session with a bare JID
343 # FIXME upstream: apparently pyjamas doesn't implement setdefault well, it ignores JID.__hash__ redefinition
344 #context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid))
345 if other_jid not in self.contexts:
346 self.contexts[other_jid] = Context(self.host, self.account, other_jid)
347 return self.contexts[other_jid]
348
349 def getContextForUser(self, other_jid, start=True):
350 """Get the context for the given JID
351
352 @param other_jid (jid.JID): your correspondent
353 @param start (bool): start non-existing context if True
354 @return: Context
355 """
356 try:
357 other_jid = self.fixResource(other_jid)
358 except NotConnectedEntity:
359 log.debug(u"getContextForUser [%s]: not connected!" % other_jid)
360 return None
361 log.debug(u"getContextForUser [%s]" % other_jid)
362 if start:
363 return self.startContext(other_jid)
364 else:
365 return self.contexts.get(other_jid, None)
366
367 def getContextsForBareUser(self, bare_jid):
368 """Get all the contexts for the users sharing the given bare JID.
369
370 @param bare_jid (jid.JID): bare JID
371 @return: list[Context]
372 """
373 return [context for other_jid, context in self.contexts.iteritems() if other_jid.bare == bare_jid]
374
375 def fixResource(self, other_jid):
376 """Return the full JID in case the resource of the given JID is missing.
377
378 @param other_jid (jid.JID): JID to check
379 @return jid.JID
380 """
381 if other_jid.resource:
382 return other_jid
383 clist = self.host.contact_list
384 if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None:
385 raise NotConnectedEntity
386 return clist.getFullJid(other_jid)
387
388
389 class OTR(object):
390
391 def __init__(self, host):
392 log.info(_(u"OTR plugin initialization"))
393 self.host = host
394 self.context_manager = None
395 self.host.bridge._registerMethods(["skipOTR"])
396 self.host.trigger.add("messageNewTrigger", self.newMessageTg, priority=trigger.TriggerManager.MAX_PRIORITY)
397 self.host.trigger.add("messageSendTrigger", self.sendMessageTg, priority=trigger.TriggerManager.MAX_PRIORITY)
398
399 # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword)
400 self._profilePluggedListener = self.profilePluggedListener
401 self._gotMenusListener = self.gotMenusListener
402 # FIXME: these listeners are never removed, can't be removed by themselves (it modifies the list while looping), maybe need a 'one_shot' argument
403 self.host.addListener('profilePlugged', self._profilePluggedListener)
404 self.host.addListener('gotMenus', self._gotMenusListener)
405
406 @classmethod
407 def getInfoText(self, state=otr.context.STATE_PLAINTEXT, trust=''):
408 """Get the widget info text for a certain message state and trust.
409
410 @param state (unicode): message state
411 @param trust (unicode): trust
412 @return: unicode
413 """
414 if not state:
415 state = OTR_MSG_STATES.keys()[0]
416 return OTR_MSG_STATES[state][1 if trust else 0]
417
418 def getInfoTextForUser(self, other_jid):
419 """Get the current info text for a conversation.
420
421 @param other_jid (jid.JID): JID of the correspondant
422 """
423 otrctx = self.context_manager.getContextForUser(other_jid, start=False)
424 if otrctx is None:
425 return OTR.getInfoText()
426 else:
427 return OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust())
428
429 def gotMenusListener(self,):
430 # TODO: get menus paths to hook directly from backend's OTR plugin
431 self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Start/Refresh")), callback=self._startRefresh)
432 self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"End session")), callback=self._endSession)
433 self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Authenticate")), callback=self._authenticate)
434 self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Drop private key")), callback=self._dropPrivkey)
435
436 def profilePluggedListener(self, profile):
437 # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword)
438 self._presenceListener = self.presenceListener
439 self._disconnectListener = self.disconnectListener
440 self.host.addListener('presence', self._presenceListener, [C.PROF_KEY_NONE])
441 # FIXME: this listener is never removed, can't be removed by itself (it modifies the list while looping), maybe need a 'one_shot' argument
442 self.host.addListener('disconnect', self._disconnectListener, [C.PROF_KEY_NONE])
443
444 self.host.bridge.call('skipOTR', None)
445 self.context_manager = ContextManager(self.host)
446 # TODO: retrieve the encrypted private key from a HTML5 persistent storage,
447 # decrypt it, parse it with otr.crypt.PK.parsePrivateKey(privkey) and
448 # assign it to self.context_manager.account.privkey
449
450 def disconnectListener(self, profile):
451 """Things to do just before the profile disconnection"""
452 self.host.removeListener('presence', self._presenceListener)
453
454 for context in self.context_manager.contexts.values():
455 context.disconnect() # FIXME: no time to send the message before the profile has been disconnected
456
457 def presenceListener(self, entity, show, priority, statuses, profile):
458 if show == C.PRESENCE_UNAVAILABLE:
459 self.endSession(entity, disconnect=False)
460
461 def newMessageTg(self, uid, timestamp, from_jid, to_jid, msg, subject, msg_type, extra, profile):
462 if msg_type != C.MESS_TYPE_CHAT:
463 return True
464
465 try:
466 msg = msg.values()[0] # FIXME: Q&D fix for message refactoring, message is now a dict
467 except IndexError:
468 return True
469 tag = otr.proto.checkForOTR(msg)
470 if tag is None or (tag == otr.context.WHITESPACE_TAG and not DEFAULT_POLICY_FLAGS['WHITESPACE_START_AKE']):
471 return True
472
473 other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid
474 otrctx = self.context_manager.getContextForUser(other_jid, start=False)
475 if otrctx is None:
476 def confirm(confirm):
477 if confirm:
478 self.host.displayWidget(chat.Chat, other_jid)
479 self.context_manager.startContext(other_jid).receiveMessage(msg)
480 else:
481 # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True
482 pass
483 key = self.context_manager.account.privkey
484 question = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM
485 dialog.ConfirmDialog(confirm, question.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
486 else: # do not ask for user confirmation if the context exist
487 otrctx.receiveMessage(msg)
488
489 return False # interrupt the main process
490
491 def sendMessageTg(self, to_jid, message, subject, mess_type, extra, callback, errback, profile_key):
492 if mess_type != C.MESS_TYPE_CHAT:
493 return True
494
495 otrctx = self.context_manager.getContextForUser(to_jid, start=False)
496 if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT:
497 if otrctx.state == otr.context.STATE_ENCRYPTED:
498 log.debug(u"encrypting message")
499 otrctx.sendMessage(message)
500 uuid_ = str(uuid.uuid4()) # FIXME
501 self.host.newMessageHandler(uuid_, time.time(), unicode(self.host.whoami), unicode(to_jid), {'': message}, {}, mess_type, extra)
502 else:
503 feedback = SEND_PLAIN_IN_FINISHED_CONTEXT
504 dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show()
505 return False # interrupt the main process
506
507 log.debug(u"sending message unencrypted")
508 return True
509
510 def endSession(self, other_jid, disconnect=True):
511 """Finish or disconnect an OTR session
512
513 @param other_jid (jid.JID): other JID
514 @param disconnect (bool): if False, finish the session but do not disconnect it
515 """
516 # checking for private key existence is not needed, context checking is enough
517 if other_jid.resource:
518 contexts = [self.context_manager.getContextForUser(other_jid, start=False)]
519 else: # contact disconnected itself so we need to terminate the OTR session but the Chat panel lost its resource
520 contexts = self.context_manager.getContextsForBareUser(other_jid)
521 for otrctx in contexts:
522 if otrctx is None or otrctx.state == otr.context.STATE_PLAINTEXT:
523 if disconnect:
524 uuid_ = str(uuid.uuid4()) # FIXME
525 self.host.newMessageHandler(uuid_, time.time(), unicode(other_jid), unicode(self.host.whoami), {'': END_PLAIN_HAS_NOT.format(jid=other_jid)}, {}, C.MESS_TYPE_INFO, {})
526 return
527 if disconnect:
528 otrctx.disconnect()
529 else:
530 otrctx.finish()
531
532 # Menu callbacks
533
534 def _startRefresh(self, caller, menu_data, profile):
535 """Start or refresh an OTR session
536
537 @param menu_data: %(menu_data)s
538 """
539 def query(other_jid):
540 otrctx = self.context_manager.getContextForUser(other_jid)
541 if otrctx:
542 otrctx.sendQueryMessage()
543
544 other_jid = jid.JID(menu_data['jid'])
545 clist = self.host.contact_list
546 if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None:
547 dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show()
548 return
549
550 key = self.context_manager.account.privkey
551 if key is None:
552 def confirm(confirm):
553 if confirm:
554 query(other_jid)
555 msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM
556 dialog.ConfirmDialog(confirm, msg.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
557 else: # on query reception we ask always, if we initiate we just ask the first time
558 query(other_jid)
559
560 def _endSession(self, caller, menu_data, profile):
561 """End an OTR session
562
563 @param menu_data: %(menu_data)s
564 """
565 self.endSession(jid.JID(menu_data['jid']))
566
567 def _authenticate(self, caller, menu_data, profile):
568 """Authenticate other user and see our own fingerprint
569
570 @param menu_data: %(menu_data)s
571 @param profile: %(doc_profile)s
572 """
573 def not_available():
574 dialog.InfoDialog(AUTH_TRUST_NA_TITLE, AUTH_TRUST_NA_TXT, AddStyleName="maxWidthLimit").show()
575
576 to_jid = jid.JID(menu_data['jid'])
577
578 # checking for private key existence is not needed, context checking is enough
579 otrctx = self.context_manager.getContextForUser(to_jid, start=False)
580 if otrctx is None or otrctx.state != otr.context.STATE_ENCRYPTED:
581 not_available()
582 return
583 otr_version = otrctx.getUsedVersion()
584 if otr_version == otr.context.OTR_VERSION_2:
585 otrctx.fingerprintAuthCb()
586 elif otr_version == otr.context.OTR_VERSION_3:
587 otrctx.smpAuthCb('question', None, 'asked')
588 else:
589 not_available()
590
591 def _dropPrivkey(self, caller, menu_data, profile):
592 """Drop our private Key
593
594 @param menu_data: %(menu_data)s
595 @param profile: %(doc_profile)s
596 """
597 priv_key = self.context_manager.account.privkey
598 if priv_key is None:
599 # we have no private key yet
600 dialog.InfoDialog(KEY_NA_TITLE, KEY_NA_TXT, AddStyleName="maxWidthLimit").show()
601 return
602
603 def dropKey(confirm):
604 if confirm:
605 # we end all sessions
606 for context in self.context_manager.contexts.values():
607 context.disconnect()
608 self.context_manager.contexts.clear()
609 self.context_manager.account.privkey = None
610 dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show()
611
612 dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show()