Mercurial > libervia-backend
comparison src/core/sat_main.py @ 1030:15f43b54d697
core, memory, bridge: added profile password + password encryption:
/!\ This changeset updates the database version to 2 and modify the database content!
Description:
- new parameter General / Password to store the profile password
- profile password is initialized with XMPP password value, it is stored hashed
- bridge methods asyncCreateProfile/asyncConnect takes a new argument "password" (default = "")
- bridge method asyncConnect returns a boolean (True = connection already established, False = connection initiated)
- profile password is checked before initializing the XMPP connection
- new private individual parameter to store the personal encryption key of each profile
- personal key is randomly generated and encrypted with the profile password
- personal key is decrypted after profile authentification and stored in a Sessions instance
- personal key is used to encrypt/decrypt other passwords when they need to be retrieved/modified
- modifying the profile password re-encrypt the personal key
- Memory.setParam now returns a Deferred (the bridge method "setParam" is unchanged)
- Memory.asyncGetParamA eventually decrypts the password, Memory.getParamA would fail on a password parameter
TODO:
- if profile authentication is OK but XMPP authentication is KO, prompt the user for another XMPP password
- fix the method "registerNewAccount" (and move it to a plugin)
- remove bridge method "connect", sole "asyncConnect" should be used
author | souliane <souliane@mailoo.org> |
---|---|
date | Wed, 07 May 2014 16:02:23 +0200 |
parents | ee46515a12f2 |
children | b262ae6d53af |
comparison
equal
deleted
inserted
replaced
1029:f6182f6418ea | 1030:15f43b54d697 |
---|---|
29 from sat.core import exceptions | 29 from sat.core import exceptions |
30 from sat.core.log import getLogger | 30 from sat.core.log import getLogger |
31 log = getLogger(__name__) | 31 log = getLogger(__name__) |
32 from sat.core.constants import Const as C | 32 from sat.core.constants import Const as C |
33 from sat.memory.memory import Memory | 33 from sat.memory.memory import Memory |
34 from sat.memory.crypto import PasswordHasher | |
34 from sat.tools.misc import TriggerManager | 35 from sat.tools.misc import TriggerManager |
35 from sat.stdui import ui_contact_list | 36 from sat.stdui import ui_contact_list |
36 from glob import glob | 37 from glob import glob |
37 from uuid import uuid4 | 38 from uuid import uuid4 |
38 import sys | 39 import sys |
196 self.plugins[import_name].is_handler = True | 197 self.plugins[import_name].is_handler = True |
197 else: | 198 else: |
198 self.plugins[import_name].is_handler = False | 199 self.plugins[import_name].is_handler = False |
199 #TODO: test xmppclient presence and register handler parent | 200 #TODO: test xmppclient presence and register handler parent |
200 | 201 |
201 def connect(self, profile_key=C.PROF_KEY_NONE): | 202 def connect(self, profile_key=C.PROF_KEY_NONE, password=''): |
202 """Connect to jabber server""" | 203 """Connect to jabber server |
203 self.asyncConnect(profile_key) | 204 |
204 | 205 @param password (string): the SàT profile password |
205 def asyncConnect(self, profile_key=C.PROF_KEY_NONE): | 206 @param profile_key: %(doc_profile_key)s |
206 """Connect to jabber server with asynchronous reply | 207 """ |
207 @param profile_key: %(doc_profile)s | 208 self.asyncConnect(profile_key, password) |
209 | |
210 def asyncConnect(self, profile_key=C.PROF_KEY_NONE, password=''): | |
211 """Retrieve the individual parameters, authenticate the profile | |
212 and initiate the connection to the associated XMPP server. | |
213 | |
214 @param password (string): the SàT profile password | |
215 @param profile_key: %(doc_profile_key)s | |
216 @return: Deferred: | |
217 - a deferred boolean if the profile authentication succeed: | |
218 - True if the XMPP connection was already established | |
219 - False if the XMPP connection has been initiated (it may still fail) | |
220 - a Failure if the profile authentication failed | |
208 """ | 221 """ |
209 def backendInitialised(dummy): | 222 def backendInitialised(dummy): |
210 profile = self.memory.getProfileName(profile_key) | 223 profile = self.memory.getProfileName(profile_key) |
211 if not profile: | 224 if not profile: |
212 log.error(_('Trying to connect a non-existant profile')) | 225 log.error(_('Trying to connect a non-existant profile')) |
213 raise exceptions.ProfileUnknownError(profile_key) | 226 raise exceptions.ProfileUnknownError(profile_key) |
214 | 227 |
215 if self.isConnected(profile): | 228 def connectXMPPClient(dummy=None): |
216 log.info(_("already connected !")) | 229 if self.isConnected(profile): |
217 return defer.succeed("None") | 230 log.info(_("already connected !")) |
218 | 231 return True |
219 def afterMemoryInit(ignore): | 232 self.memory.startProfileSession(profile) |
220 """This part must be called when we have loaded individual parameters from memory""" | 233 d = self.memory.loadIndividualParams(profile) |
221 try: | 234 d.addCallback(lambda dummy: self._connectXMPPClient(profile)) |
222 port = int(self.memory.getParamA("Port", "Connection", profile_key=profile)) | 235 return d.addCallback(lambda dummy: False) |
223 except ValueError: | 236 |
224 log.error(_("Can't parse port value, using default value")) | 237 d = self._authenticateProfile(password, profile) |
225 port = 5222 | 238 d.addCallback(connectXMPPClient) |
226 | 239 return d |
227 current = self.profiles[profile] = xmpp.SatXMPPClient( | 240 |
228 self, profile, | |
229 jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key=profile), profile), | |
230 self.memory.getParamA("Password", "Connection", profile_key=profile), | |
231 self.memory.getParamA("Server", "Connection", profile_key=profile), | |
232 port) | |
233 | |
234 current.messageProt = xmpp.SatMessageProtocol(self) | |
235 current.messageProt.setHandlerParent(current) | |
236 | |
237 current.roster = xmpp.SatRosterProtocol(self) | |
238 current.roster.setHandlerParent(current) | |
239 | |
240 current.presence = xmpp.SatPresenceProtocol(self) | |
241 current.presence.setHandlerParent(current) | |
242 | |
243 current.fallBack = xmpp.SatFallbackHandler(self) | |
244 current.fallBack.setHandlerParent(current) | |
245 | |
246 current.versionHandler = xmpp.SatVersionHandler(C.APP_NAME_FULL, | |
247 C.APP_VERSION) | |
248 current.versionHandler.setHandlerParent(current) | |
249 | |
250 current.identityHandler = xmpp.SatIdentityHandler() | |
251 current.identityHandler.setHandlerParent(current) | |
252 | |
253 log.debug(_("setting plugins parents")) | |
254 | |
255 plugin_conn_cb = [] | |
256 for plugin in self.plugins.iteritems(): | |
257 if plugin[1].is_handler: | |
258 plugin[1].getHandler(profile).setHandlerParent(current) | |
259 connected_cb = getattr(plugin[1], "profileConnected", None) | |
260 if connected_cb: | |
261 plugin_conn_cb.append((plugin[0], connected_cb)) | |
262 | |
263 current.startService() | |
264 | |
265 d = current.getConnectionDeferred() | |
266 d.addCallback(lambda dummy: current.roster.got_roster) # we want to be sure that we got the roster | |
267 | |
268 def pluginsConnection(dummy): | |
269 """Call profileConnected callback for all plugins, and print error message if any of them fails""" | |
270 conn_cb_list = [] | |
271 for dummy, callback in plugin_conn_cb: | |
272 conn_cb_list.append(defer.maybeDeferred(callback, profile)) | |
273 list_d = defer.DeferredList(conn_cb_list) | |
274 | |
275 def logPluginResults(results): | |
276 all_succeed = all([success for success, result in results]) | |
277 if not all_succeed: | |
278 log.error(_("Plugins initialisation error")) | |
279 for idx, (success, result) in enumerate(results): | |
280 if not success: | |
281 log.error("error (plugin %(name)s): %(failure)s" % {'name': plugin_conn_cb[idx][0], | |
282 'failure': result}) | |
283 | |
284 list_d.addCallback(logPluginResults) | |
285 return list_d | |
286 | |
287 d.addCallback(pluginsConnection) | |
288 return d | |
289 | |
290 self.memory.startProfileSession(profile) | |
291 return self.memory.loadIndividualParams(profile).addCallback(afterMemoryInit) | |
292 return self._initialised.addCallback(backendInitialised) | 241 return self._initialised.addCallback(backendInitialised) |
242 | |
243 @defer.inlineCallbacks | |
244 def _connectXMPPClient(self, profile): | |
245 """This part is called from asyncConnect when we have loaded individual parameters from memory""" | |
246 try: | |
247 port = int(self.memory.getParamA("Port", "Connection", profile_key=profile)) | |
248 except ValueError: | |
249 log.error(_("Can't parse port value, using default value")) | |
250 port = 5222 | |
251 | |
252 password = yield self.memory.asyncGetParamA("Password", "Connection", profile_key=profile) | |
253 current = self.profiles[profile] = xmpp.SatXMPPClient(self, profile, | |
254 jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key=profile)), | |
255 password, self.memory.getParamA("Server", "Connection", profile_key=profile), port) | |
256 | |
257 current.messageProt = xmpp.SatMessageProtocol(self) | |
258 current.messageProt.setHandlerParent(current) | |
259 | |
260 current.roster = xmpp.SatRosterProtocol(self) | |
261 current.roster.setHandlerParent(current) | |
262 | |
263 current.presence = xmpp.SatPresenceProtocol(self) | |
264 current.presence.setHandlerParent(current) | |
265 | |
266 current.fallBack = xmpp.SatFallbackHandler(self) | |
267 current.fallBack.setHandlerParent(current) | |
268 | |
269 current.versionHandler = xmpp.SatVersionHandler(C.APP_NAME_FULL, | |
270 C.APP_VERSION) | |
271 current.versionHandler.setHandlerParent(current) | |
272 | |
273 current.identityHandler = xmpp.SatIdentityHandler() | |
274 current.identityHandler.setHandlerParent(current) | |
275 | |
276 log.debug(_("setting plugins parents")) | |
277 | |
278 plugin_conn_cb = [] | |
279 for plugin in self.plugins.iteritems(): | |
280 if plugin[1].is_handler: | |
281 plugin[1].getHandler(profile).setHandlerParent(current) | |
282 connected_cb = getattr(plugin[1], "profileConnected", None) | |
283 if connected_cb: | |
284 plugin_conn_cb.append((plugin[0], connected_cb)) | |
285 | |
286 current.startService() | |
287 | |
288 yield current.getConnectionDeferred() | |
289 yield current.roster.got_roster # we want to be sure that we got the roster | |
290 | |
291 # Call profileConnected callback for all plugins, and print error message if any of them fails | |
292 conn_cb_list = [] | |
293 for dummy, callback in plugin_conn_cb: | |
294 conn_cb_list.append(defer.maybeDeferred(callback, profile)) | |
295 list_d = defer.DeferredList(conn_cb_list) | |
296 | |
297 def logPluginResults(results): | |
298 all_succeed = all([success for success, result in results]) | |
299 if not all_succeed: | |
300 log.error(_("Plugins initialisation error")) | |
301 for idx, (success, result) in enumerate(results): | |
302 if not success: | |
303 log.error("error (plugin %(name)s): %(failure)s" % | |
304 {'name': plugin_conn_cb[idx][0], 'failure': result}) | |
305 | |
306 yield list_d.addCallback(logPluginResults) | |
307 | |
308 def _authenticateProfile(self, password, profile): | |
309 """Authenticate the profile. | |
310 | |
311 @param password (string): the SàT profile password | |
312 @param profile_key: %(doc_profile_key)s | |
313 @return: Deferred: a deferred None in case of success, a failure otherwise. | |
314 """ | |
315 session_data = self.memory.auth_sessions.profileGetUnique(profile) | |
316 if not password and session_data: | |
317 # XXX: this allows any frontend to connect with the empty password as soon as | |
318 # the profile has been authenticated at least once before. It is OK as long as | |
319 # submitting a form with empty passwords is restricted to local frontends. | |
320 return defer.succeed(None) | |
321 | |
322 def check_result(result): | |
323 if not result: | |
324 log.warning(_('Authentication failure of profile %s') % profile) | |
325 raise exceptions.PasswordError(_("The provided profile password doesn't match.")) | |
326 if not session_data: # avoid to create two profile sessions when password if specified | |
327 return self.memory.newAuthSession(password, profile) | |
328 | |
329 d = self.memory.asyncGetParamA(C.PROFILE_PASS_PATH[1], C.PROFILE_PASS_PATH[0], profile_key=profile) | |
330 d.addCallback(lambda sat_cipher: PasswordHasher.verify(password, sat_cipher)) | |
331 return d.addCallback(check_result) | |
293 | 332 |
294 def disconnect(self, profile_key): | 333 def disconnect(self, profile_key): |
295 """disconnect from jabber server""" | 334 """disconnect from jabber server""" |
296 if not self.isConnected(profile_key): | 335 if not self.isConnected(profile_key): |
297 log.info(_("not connected !")) | 336 log.info(_("not connected !")) |
386 return next_id | 425 return next_id |
387 | 426 |
388 def registerNewAccountCB(self, data, profile): | 427 def registerNewAccountCB(self, data, profile): |
389 # FIXME: to be removed/redone elsewhere | 428 # FIXME: to be removed/redone elsewhere |
390 user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0] | 429 user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0] |
391 password = self.memory.getParamA("Password", "Connection", profile_key=profile) | |
392 server = self.memory.getParamA("Server", "Connection", profile_key=profile) | 430 server = self.memory.getParamA("Server", "Connection", profile_key=profile) |
393 | 431 d = self.memory.asyncGetParamA("Password", "Connection", profile_key=profile) |
394 if not user or not password or not server: | 432 |
395 raise exceptions.DataError(_("No user, password or server given, can't register new account.")) | 433 def gotPassword(password): |
396 | 434 if not user or not password or not server: |
397 # FIXME: to be fixed with XMLUI dialogs once their implemented | 435 raise exceptions.DataError(_("No user, password or server given, can't register new account.")) |
398 confirm_id = sat_next_id() | 436 |
399 self.__private_data[confirm_id] = (id, profile) | 437 # FIXME: to be fixed with XMLUI dialogs once their implemented |
400 | 438 confirm_id = sat_next_id() |
401 self.askConfirmation( | 439 self.__private_data[confirm_id] = (id, profile) |
402 confirm_id, "YES/NO", | 440 |
403 {"message": _("Are you sure to register new account [%(user)s] to server %(server)s ?") % {'user': user, 'server': server, 'profile': profile}}, | 441 self.askConfirmation( |
404 self.regisConfirmCB, profile) | 442 confirm_id, "YES/NO", |
405 print "===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============" | 443 {"message": _("Are you sure to register new account [%(user)s] to server %(server)s ?") % {'user': user, 'server': server, 'profile': profile}}, |
444 self.regisConfirmCB, profile) | |
445 print "===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============" | |
446 | |
447 d.addCallback(gotPassword) | |
406 | 448 |
407 def regisConfirmCB(self, id, accepted, data, profile): | 449 def regisConfirmCB(self, id, accepted, data, profile): |
408 print _("register Confirmation CB ! (%s)") % str(accepted) | 450 print _("register Confirmation CB ! (%s)") % str(accepted) |
409 action_id, profile = self.__private_data[id] | 451 action_id, profile = self.__private_data[id] |
410 del self.__private_data[id] | 452 del self.__private_data[id] |
411 if accepted: | 453 if accepted: |
412 user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0] | 454 user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0] |
413 password = self.memory.getParamA("Password", "Connection", profile_key=profile) | |
414 server = self.memory.getParamA("Server", "Connection", profile_key=profile) | 455 server = self.memory.getParamA("Server", "Connection", profile_key=profile) |
415 self.registerNewAccount(user, password, None, server, id=action_id) | 456 d = self.memory.asyncGetParamA("Password", "Connection", profile_key=profile) |
457 d.addCallback(lambda password: self.registerNewAccount(user, password, None, server, id=action_id)) | |
416 else: | 458 else: |
417 self.actionResult(action_id, "SUPPRESS", {}, profile) | 459 self.actionResult(action_id, "SUPPRESS", {}, profile) |
418 | 460 |
419 ## Client management ## | 461 ## Client management ## |
420 | 462 |
421 def setParam(self, name, value, category, security_limit, profile_key): | 463 def setParam(self, name, value, category, security_limit, profile_key): |
422 """set wanted paramater and notice observers""" | 464 """set wanted paramater and notice observers""" |
423 log.info(_("setting param: %(name)s=%(value)s in category %(category)s") % {'name': name, 'value': value, 'category': category}) | |
424 self.memory.setParam(name, value, category, security_limit, profile_key) | 465 self.memory.setParam(name, value, category, security_limit, profile_key) |
425 | 466 |
426 def isConnected(self, profile_key): | 467 def isConnected(self, profile_key): |
427 """Return connection status of profile | 468 """Return connection status of profile |
428 @param profile_key: key_word or profile name to determine profile name | 469 @param profile_key: key_word or profile name to determine profile name |
429 @return True if connected | 470 @return: True if connected |
430 """ | 471 """ |
431 profile = self.memory.getProfileName(profile_key) | 472 profile = self.memory.getProfileName(profile_key) |
432 if not profile: | 473 if not profile: |
433 log.error(_('asking connection status for a non-existant profile')) | 474 log.error(_('asking connection status for a non-existant profile')) |
434 return | 475 raise exceptions.ProfileUnknownError(profile_key) |
435 if profile not in self.profiles: | 476 if profile not in self.profiles: |
436 return False | 477 return False |
437 return self.profiles[profile].isConnected() | 478 return self.profiles[profile].isConnected() |
438 | 479 |
439 | 480 |