Mercurial > libervia-backend
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.