Mercurial > libervia-web
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() |