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()