Mercurial > libervia-backend
diff 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 |
line wrap: on
line diff
--- a/src/core/sat_main.py Sat May 10 17:37:32 2014 +0200 +++ b/src/core/sat_main.py Wed May 07 16:02:23 2014 +0200 @@ -31,6 +31,7 @@ log = getLogger(__name__) from sat.core.constants import Const as C from sat.memory.memory import Memory +from sat.memory.crypto import PasswordHasher from sat.tools.misc import TriggerManager from sat.stdui import ui_contact_list from glob import glob @@ -198,13 +199,25 @@ self.plugins[import_name].is_handler = False #TODO: test xmppclient presence and register handler parent - def connect(self, profile_key=C.PROF_KEY_NONE): - """Connect to jabber server""" - self.asyncConnect(profile_key) + def connect(self, profile_key=C.PROF_KEY_NONE, password=''): + """Connect to jabber server + + @param password (string): the SàT profile password + @param profile_key: %(doc_profile_key)s + """ + self.asyncConnect(profile_key, password) - def asyncConnect(self, profile_key=C.PROF_KEY_NONE): - """Connect to jabber server with asynchronous reply - @param profile_key: %(doc_profile)s + def asyncConnect(self, profile_key=C.PROF_KEY_NONE, password=''): + """Retrieve the individual parameters, authenticate the profile + and initiate the connection to the associated XMPP server. + + @param password (string): the SàT profile password + @param profile_key: %(doc_profile_key)s + @return: Deferred: + - a deferred boolean if the profile authentication succeed: + - True if the XMPP connection was already established + - False if the XMPP connection has been initiated (it may still fail) + - a Failure if the profile authentication failed """ def backendInitialised(dummy): profile = self.memory.getProfileName(profile_key) @@ -212,84 +225,110 @@ log.error(_('Trying to connect a non-existant profile')) raise exceptions.ProfileUnknownError(profile_key) - if self.isConnected(profile): - log.info(_("already connected !")) - return defer.succeed("None") + def connectXMPPClient(dummy=None): + if self.isConnected(profile): + log.info(_("already connected !")) + return True + self.memory.startProfileSession(profile) + d = self.memory.loadIndividualParams(profile) + d.addCallback(lambda dummy: self._connectXMPPClient(profile)) + return d.addCallback(lambda dummy: False) - def afterMemoryInit(ignore): - """This part must be called when we have loaded individual parameters from memory""" - try: - port = int(self.memory.getParamA("Port", "Connection", profile_key=profile)) - except ValueError: - log.error(_("Can't parse port value, using default value")) - port = 5222 + d = self._authenticateProfile(password, profile) + d.addCallback(connectXMPPClient) + return d + + return self._initialised.addCallback(backendInitialised) - current = self.profiles[profile] = xmpp.SatXMPPClient( - self, profile, - jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key=profile), profile), - self.memory.getParamA("Password", "Connection", profile_key=profile), - self.memory.getParamA("Server", "Connection", profile_key=profile), - port) + @defer.inlineCallbacks + def _connectXMPPClient(self, profile): + """This part is called from asyncConnect when we have loaded individual parameters from memory""" + try: + port = int(self.memory.getParamA("Port", "Connection", profile_key=profile)) + except ValueError: + log.error(_("Can't parse port value, using default value")) + port = 5222 - current.messageProt = xmpp.SatMessageProtocol(self) - current.messageProt.setHandlerParent(current) + password = yield self.memory.asyncGetParamA("Password", "Connection", profile_key=profile) + current = self.profiles[profile] = xmpp.SatXMPPClient(self, profile, + jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key=profile)), + password, self.memory.getParamA("Server", "Connection", profile_key=profile), port) - current.roster = xmpp.SatRosterProtocol(self) - current.roster.setHandlerParent(current) + current.messageProt = xmpp.SatMessageProtocol(self) + current.messageProt.setHandlerParent(current) - current.presence = xmpp.SatPresenceProtocol(self) - current.presence.setHandlerParent(current) + current.roster = xmpp.SatRosterProtocol(self) + current.roster.setHandlerParent(current) - current.fallBack = xmpp.SatFallbackHandler(self) - current.fallBack.setHandlerParent(current) + current.presence = xmpp.SatPresenceProtocol(self) + current.presence.setHandlerParent(current) + + current.fallBack = xmpp.SatFallbackHandler(self) + current.fallBack.setHandlerParent(current) - current.versionHandler = xmpp.SatVersionHandler(C.APP_NAME_FULL, - C.APP_VERSION) - current.versionHandler.setHandlerParent(current) + current.versionHandler = xmpp.SatVersionHandler(C.APP_NAME_FULL, + C.APP_VERSION) + current.versionHandler.setHandlerParent(current) - current.identityHandler = xmpp.SatIdentityHandler() - current.identityHandler.setHandlerParent(current) + current.identityHandler = xmpp.SatIdentityHandler() + current.identityHandler.setHandlerParent(current) + + log.debug(_("setting plugins parents")) - log.debug(_("setting plugins parents")) + plugin_conn_cb = [] + for plugin in self.plugins.iteritems(): + if plugin[1].is_handler: + plugin[1].getHandler(profile).setHandlerParent(current) + connected_cb = getattr(plugin[1], "profileConnected", None) + if connected_cb: + plugin_conn_cb.append((plugin[0], connected_cb)) - plugin_conn_cb = [] - for plugin in self.plugins.iteritems(): - if plugin[1].is_handler: - plugin[1].getHandler(profile).setHandlerParent(current) - connected_cb = getattr(plugin[1], "profileConnected", None) - if connected_cb: - plugin_conn_cb.append((plugin[0], connected_cb)) + current.startService() + + yield current.getConnectionDeferred() + yield current.roster.got_roster # we want to be sure that we got the roster - current.startService() - - d = current.getConnectionDeferred() - d.addCallback(lambda dummy: current.roster.got_roster) # we want to be sure that we got the roster + # Call profileConnected callback for all plugins, and print error message if any of them fails + conn_cb_list = [] + for dummy, callback in plugin_conn_cb: + conn_cb_list.append(defer.maybeDeferred(callback, profile)) + list_d = defer.DeferredList(conn_cb_list) - def pluginsConnection(dummy): - """Call profileConnected callback for all plugins, and print error message if any of them fails""" - conn_cb_list = [] - for dummy, callback in plugin_conn_cb: - conn_cb_list.append(defer.maybeDeferred(callback, profile)) - list_d = defer.DeferredList(conn_cb_list) + def logPluginResults(results): + all_succeed = all([success for success, result in results]) + if not all_succeed: + log.error(_("Plugins initialisation error")) + for idx, (success, result) in enumerate(results): + if not success: + log.error("error (plugin %(name)s): %(failure)s" % + {'name': plugin_conn_cb[idx][0], 'failure': result}) + + yield list_d.addCallback(logPluginResults) + + def _authenticateProfile(self, password, profile): + """Authenticate the profile. - def logPluginResults(results): - all_succeed = all([success for success, result in results]) - if not all_succeed: - log.error(_("Plugins initialisation error")) - for idx, (success, result) in enumerate(results): - if not success: - log.error("error (plugin %(name)s): %(failure)s" % {'name': plugin_conn_cb[idx][0], - 'failure': result}) + @param password (string): the SàT profile password + @param profile_key: %(doc_profile_key)s + @return: Deferred: a deferred None in case of success, a failure otherwise. + """ + session_data = self.memory.auth_sessions.profileGetUnique(profile) + if not password and session_data: + # XXX: this allows any frontend to connect with the empty password as soon as + # the profile has been authenticated at least once before. It is OK as long as + # submitting a form with empty passwords is restricted to local frontends. + return defer.succeed(None) - list_d.addCallback(logPluginResults) - return list_d + def check_result(result): + if not result: + log.warning(_('Authentication failure of profile %s') % profile) + raise exceptions.PasswordError(_("The provided profile password doesn't match.")) + if not session_data: # avoid to create two profile sessions when password if specified + return self.memory.newAuthSession(password, profile) - d.addCallback(pluginsConnection) - return d - - self.memory.startProfileSession(profile) - return self.memory.loadIndividualParams(profile).addCallback(afterMemoryInit) - return self._initialised.addCallback(backendInitialised) + d = self.memory.asyncGetParamA(C.PROFILE_PASS_PATH[1], C.PROFILE_PASS_PATH[0], profile_key=profile) + d.addCallback(lambda sat_cipher: PasswordHasher.verify(password, sat_cipher)) + return d.addCallback(check_result) def disconnect(self, profile_key): """disconnect from jabber server""" @@ -388,21 +427,24 @@ def registerNewAccountCB(self, data, profile): # FIXME: to be removed/redone elsewhere user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0] - password = self.memory.getParamA("Password", "Connection", profile_key=profile) server = self.memory.getParamA("Server", "Connection", profile_key=profile) + d = self.memory.asyncGetParamA("Password", "Connection", profile_key=profile) - if not user or not password or not server: - raise exceptions.DataError(_("No user, password or server given, can't register new account.")) + def gotPassword(password): + if not user or not password or not server: + raise exceptions.DataError(_("No user, password or server given, can't register new account.")) - # FIXME: to be fixed with XMLUI dialogs once their implemented - confirm_id = sat_next_id() - self.__private_data[confirm_id] = (id, profile) + # FIXME: to be fixed with XMLUI dialogs once their implemented + confirm_id = sat_next_id() + self.__private_data[confirm_id] = (id, profile) - self.askConfirmation( - confirm_id, "YES/NO", - {"message": _("Are you sure to register new account [%(user)s] to server %(server)s ?") % {'user': user, 'server': server, 'profile': profile}}, - self.regisConfirmCB, profile) - print "===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============" + self.askConfirmation( + confirm_id, "YES/NO", + {"message": _("Are you sure to register new account [%(user)s] to server %(server)s ?") % {'user': user, 'server': server, 'profile': profile}}, + self.regisConfirmCB, profile) + print "===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============" + + d.addCallback(gotPassword) def regisConfirmCB(self, id, accepted, data, profile): print _("register Confirmation CB ! (%s)") % str(accepted) @@ -410,9 +452,9 @@ del self.__private_data[id] if accepted: user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0] - password = self.memory.getParamA("Password", "Connection", profile_key=profile) server = self.memory.getParamA("Server", "Connection", profile_key=profile) - self.registerNewAccount(user, password, None, server, id=action_id) + d = self.memory.asyncGetParamA("Password", "Connection", profile_key=profile) + d.addCallback(lambda password: self.registerNewAccount(user, password, None, server, id=action_id)) else: self.actionResult(action_id, "SUPPRESS", {}, profile) @@ -420,18 +462,17 @@ def setParam(self, name, value, category, security_limit, profile_key): """set wanted paramater and notice observers""" - log.info(_("setting param: %(name)s=%(value)s in category %(category)s") % {'name': name, 'value': value, 'category': category}) self.memory.setParam(name, value, category, security_limit, profile_key) def isConnected(self, profile_key): """Return connection status of profile @param profile_key: key_word or profile name to determine profile name - @return True if connected + @return: True if connected """ profile = self.memory.getProfileName(profile_key) if not profile: log.error(_('asking connection status for a non-existant profile')) - return + raise exceptions.ProfileUnknownError(profile_key) if profile not in self.profiles: return False return self.profiles[profile].isConnected()