diff src/memory/params.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 8bae81e254a2
children 6e975c6b0faf
line wrap: on
line diff
--- a/src/memory/params.py	Sat May 10 17:37:32 2014 +0200
+++ b/src/memory/params.py	Wed May 07 16:02:23 2014 +0200
@@ -21,6 +21,7 @@
 
 from sat.core import exceptions
 from sat.core.constants import Const as C
+from sat.memory.crypto import BlockCipher, PasswordHasher
 from xml.dom import minidom, NotFoundErr
 from sat.core.log import getLogger
 log = getLogger(__name__)
@@ -41,6 +42,9 @@
     <general>
     </general>
     <individual>
+        <category name="General" label="%(category_general)s">
+            <param name="Password" value="" type="password" />
+        </category>
         <category name="Connection" label="%(category_connection)s">
             <param name="JabberID" value="name@example.org/SàT" type="string" />
             <param name="Password" value="" type="password" />
@@ -57,6 +61,7 @@
     </individual>
     </params>
     """ % {
+        'category_general': _("General"),
         'category_connection': _("Connection"),
         'label_NewAccount': _("Register new account"),
         'label_autoconnect': _('Connect on frontend startup'),
@@ -311,11 +316,16 @@
                 d.addCallback(self.__default_ok, name, category)
                 d.addErrback(errback or self.__default_ko, name, category)
 
-    def _getAttr(self, node, attr, value):
-        """ get attribute value
+    def _getAttr_internal(self, node, attr, value):
+        """Get attribute value.
+
+        /!\ This method would return encrypted password values.
+
         @param node: XML param node
         @param attr: name of the attribute to get (e.g.: 'value' or 'type')
-        @param value: user defined value"""
+        @param value: user defined value
+        @return: str
+        """
         if attr == 'value':
             value_to_use = value if value is not None else node.getAttribute(attr)  # we use value (user defined) if it exist, else we use node's default value
             if node.getAttribute('type') == 'bool':
@@ -323,6 +333,54 @@
             return value_to_use
         return node.getAttribute(attr)
 
+    def _getAttr(self, node, attr, value):
+        """Get attribute value (synchronous).
+
+        /!\ This method can not be used to retrieve password values.
+
+        @param node: XML param node
+        @param attr: name of the attribute to get (e.g.: 'value' or 'type')
+        @param value: user defined value
+        @return: str
+        """
+        if attr == 'value' and node.getAttribute('type') == 'password':
+            raise exceptions.InternalError('To retrieve password values, use _asyncGetAttr instead of _getAttr')
+        return self._getAttr_internal(node, attr, value)
+
+    def _asyncGetAttr(self, node, attr, value, profile=None):
+        """Get attribute value.
+
+        Profile passwords are returned hashed (if not empty),
+        other passwords are returned decrypted (if not empty).
+
+        @param node: XML param node
+        @param attr: name of the attribute to get (e.g.: 'value' or 'type')
+        @param value: user defined value
+        @param profile: %(doc_profile)s
+        @return: a deferred str
+        """
+        value = self._getAttr_internal(node, attr, value)
+        if attr != 'value' or node.getAttribute('type') != 'password':
+            return defer.succeed(value)
+        param_cat = node.parentNode.getAttribute('name')
+        param_name = node.getAttribute('name')
+        if ((param_cat, param_name) == C.PROFILE_PASS_PATH) or not value:
+            return defer.succeed(value)  # profile password and empty passwords are returned "as is"
+        if not profile:
+            raise exceptions.ProfileUnknownError('The profile is needed to decrypt a password')
+        try:
+            personal_key = self.host.memory.auth_sessions.profileGetUnique(profile)[C.MEMORY_CRYPTO_KEY]
+        except TypeError:
+            raise exceptions.InternalError(_('Trying to decrypt a password while the personal key is undefined!'))
+        d = BlockCipher.decrypt(personal_key, value)
+
+        def gotPlainPassword(password):
+            if password is None:  # empty value means empty password, None means decryption failure
+                raise exceptions.InternalError(_('The stored password could not be decrypted!'))
+            return password
+
+        return d.addCallback(gotPlainPassword)
+
     def __type_to_string(self, result):
         """ convert result to string, according to its type """
         if isinstance(result, bool):
@@ -334,19 +392,26 @@
         return self.__type_to_string(self.getParamA(name, category, attr, profile_key))
 
     def getParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
-        """Helper method to get a specific attribute
-           @param name: name of the parameter
-           @param category: category of the parameter
-           @param attr: name of the attribute (default: "value")
-           @param profile: owner of the param (@ALL@ for everyone)
+        """Helper method to get a specific attribute.
+
+        /!\ This method would return encrypted password values,
+            to get the plain values you have to use _asyncGetParamA.
 
-           @return: attribute"""
+       @param name: name of the parameter
+       @param category: category of the parameter
+       @param attr: name of the attribute (default: "value")
+       @param profile: owner of the param (@ALL@ for everyone)
+       @return: attribute
+       """
         #FIXME: looks really dirty and buggy, need to be reviewed/refactored
         node = self._getParamNode(name, category)
         if not node:
             log.error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category})
             raise exceptions.NotFound
 
+        if attr == 'value' and node[1].getAttribute('type') == 'password':
+            raise exceptions.InternalError('To retrieve password values, use asyncGetParamA instead of getParamA')
+
         if node[0] == C.GENERAL:
             value = self._getParam(category, name, C.GENERAL)
             return self._getAttr(node[1], attr, value)
@@ -372,41 +437,42 @@
         return d
 
     def asyncGetParamA(self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE):
-        """Helper method to get a specific attribute
-           @param name: name of the parameter
-           @param category: category of the parameter
-           @param attr: name of the attribute (default: "value")
-           @param profile: owner of the param (@ALL@ for everyone)"""
+        """Helper method to get a specific attribute.
+        @param name: name of the parameter
+        @param category: category of the parameter
+        @param attr: name of the attribute (default: "value")
+        @param profile: owner of the param (@ALL@ for everyone)
+        @return: Deferred
+        """
         node = self._getParamNode(name, category)
         if not node:
             log.error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category})
-            return None
+            return defer.succeed(None)
 
         if not self.checkSecurityLimit(node[1], security_limit):
             log.warning(_("Trying to get parameter '%(param)s' in category '%(cat)s' without authorization!!!"
                       % {'param': name, 'cat': category}))
-            return None
+            return defer.succeed(None)
 
         if node[0] == C.GENERAL:
             value = self._getParam(category, name, C.GENERAL)
-            return defer.succeed(self._getAttr(node[1], attr, value))
+            return self._asyncGetAttr(node[1], attr, value)
 
         assert node[0] == C.INDIVIDUAL
 
         profile = self.getProfileName(profile_key)
         if not profile:
-            log.error(_('Requesting a param for a non-existant profile'))
-            return defer.fail()
+            raise exceptions.InternalError(_('Requesting a param for a non-existant profile'))
 
         if attr != "value":
             return defer.succeed(node[1].getAttribute(attr))
         try:
             value = self._getParam(category, name, profile=profile)
-            return defer.succeed(self._getAttr(node[1], attr, value))
+            return self._asyncGetAttr(node[1], attr, value, profile)
         except exceptions.ProfileNotInCacheError:
             #We have to ask data to the storage manager
             d = self.storage.getIndParam(category, name, profile)
-            return d.addCallback(lambda value: self._getAttr(node[1], attr, value))
+            return d.addCallback(lambda value: self._asyncGetAttr(node[1], attr, value, profile))
 
     def _getParam(self, category, name, type_=C.INDIVIDUAL, cache=None, profile=C.PROF_KEY_NONE):
         """Return the param, or None if it doesn't exist
@@ -625,24 +691,39 @@
         return categories
 
     def setParam(self, name, value, category, security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE):
-        """Set a parameter, return None if the parameter is not in param xml"""
-        #TODO: use different behaviour depending of the data type (e.g. password encrypted)
+        """Set a parameter, return None if the parameter is not in param xml.
+
+        Parameter of type 'password' that are not the SàT profile password are
+        stored encrypted (if not empty). The profile password is stored hashed
+        (if not empty).
+
+        @param name (str): the parameter name
+        @param value (str): the new value
+        @param category (str): the parameter category
+        @param security_limit (int)
+        @param profile_key (str): %(doc_profile_key)s
+        @return: a deferred None value when everything is done
+        """
         if profile_key != C.PROF_KEY_NONE:
             profile = self.getProfileName(profile_key)
             if not profile:
                 log.error(_('Trying to set parameter for an unknown profile'))
-                return  # TODO: throw an error
+                raise exceptions.ProfileUnknownError
 
         node = self._getParamNode(name, category, '@ALL@')
         if not node:
             log.error(_('Requesting an unknown parameter (%(category)s/%(name)s)')
                   % {'category': category, 'name': name})
-            return
+            return defer.succeed(None)
 
         if not self.checkSecurityLimit(node[1], security_limit):
             log.warning(_("Trying to set parameter '%(param)s' in category '%(cat)s' without authorization!!!"
                           % {'param': name, 'cat': category}))
-            return
+            return defer.succeed(None)
+
+        type_ = node[1].getAttribute("type")
+        log.info(_("Setting parameter (%(category)s, %(name)s) = %(value)s") %
+                 {'category': category, 'name': name, 'value': value if type_ != 'password' else '********'})
 
         if node[0] == C.GENERAL:
             self.params_gen[(category, name)] = value
@@ -651,20 +732,67 @@
                 if self.host.isConnected(profile):
                     self.host.bridge.paramUpdate(name, value, category, profile)
                     self.host.trigger.point("paramUpdateTrigger", name, value, category, node[0], profile)
-            return
+            return defer.succeed(None)
 
         assert (node[0] == C.INDIVIDUAL)
         assert (profile_key != C.PROF_KEY_NONE)
 
-        type_ = node[1].getAttribute("type")
+        d_list = []
         if type_ == "button":
-            print "clique", node.toxml()
+            log.debug("Clicked param button %s" % node.toxml())
+            return defer.succeed(None)
+        elif type_ == "password":
+            try:
+                personal_key = self.host.memory.auth_sessions.profileGetUnique(profile)[C.MEMORY_CRYPTO_KEY]
+            except TypeError:
+                raise exceptions.InternalError(_('Trying to encrypt a password while the personal key is undefined!'))
+            if (category, name) == C.PROFILE_PASS_PATH:
+                # using 'value' as the encryption key to encrypt another encryption key... could be confusing!
+                d_list.append(self.host.memory.encryptPersonalData(data_key=C.MEMORY_CRYPTO_KEY,
+                                                                    data_value=personal_key,
+                                                                    crypto_key=value,
+                                                                    profile=profile))
+                d = PasswordHasher.hash(value)  # profile password is hashed (empty value stays empty)
+            elif value:  # other non empty passwords are encrypted with the personal key
+                d = BlockCipher.encrypt(personal_key, value)
         else:
+            d = defer.succeed(value)
+
+        def gotFinalValue(value):
             if self.host.isConnected(profile):  # key can not exists if profile is not connected
                 self.params[profile][(category, name)] = value
             self.host.bridge.paramUpdate(name, value, category, profile)
             self.host.trigger.point("paramUpdateTrigger", name, value, category, node[0], profile)
-            self.storage.setIndParam(category, name, value, profile)
+            d_list.append(self.storage.setIndParam(category, name, value, profile))
+
+        d.addCallback(gotFinalValue)
+        return defer.DeferredList(d_list).addCallback(lambda dummy: None)
+
+    def _getNodesOfTypes(self, attr_type, node_type="@ALL@"):
+        """Return all the nodes matching the given types.
+
+        TODO: using during the dev but not anymore... remove if not needed
+
+        @param attr_type (str): the attribute type (string, text, password, bool, button, list)
+        @param node_type (str): keyword for filtering:
+                                    @ALL@ search everywhere
+                                    @GENERAL@ only search in general type
+                                    @INDIVIDUAL@ only search in individual type
+        @return: dict{tuple: node}: a dict {key, value} where:
+            - key is a couple (attribute category, attribute name)
+            - value is a node
+        """
+        ret = {}
+        for type_node in self.dom.documentElement.childNodes:
+            if (((node_type == "@ALL@" or node_type == "@GENERAL@") and type_node.nodeName == C.GENERAL) or
+                ((node_type == "@ALL@" or node_type == "@INDIVIDUAL@") and type_node.nodeName == C.INDIVIDUAL)):
+                for cat_node in type_node.getElementsByTagName('category'):
+                    cat = cat_node.getAttribute('name')
+                    params = cat_node.getElementsByTagName("param")
+                    for param in params:
+                        if param.getAttribute("type") == attr_type:
+                            ret[(cat, param.getAttribute("name"))] = param
+        return ret
 
     def checkSecurityLimit(self, node, security_limit):
         """Check the given node against the given security limit.