Mercurial > libervia-backend
comparison 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 |
comparison
equal
deleted
inserted
replaced
1029:f6182f6418ea | 1030:15f43b54d697 |
---|---|
19 | 19 |
20 from sat.core.i18n import _ | 20 from sat.core.i18n import _ |
21 | 21 |
22 from sat.core import exceptions | 22 from sat.core import exceptions |
23 from sat.core.constants import Const as C | 23 from sat.core.constants import Const as C |
24 from sat.memory.crypto import BlockCipher, PasswordHasher | |
24 from xml.dom import minidom, NotFoundErr | 25 from xml.dom import minidom, NotFoundErr |
25 from sat.core.log import getLogger | 26 from sat.core.log import getLogger |
26 log = getLogger(__name__) | 27 log = getLogger(__name__) |
27 from twisted.internet import defer | 28 from twisted.internet import defer |
28 from twisted.python.failure import Failure | 29 from twisted.python.failure import Failure |
39 default_xml = u""" | 40 default_xml = u""" |
40 <params> | 41 <params> |
41 <general> | 42 <general> |
42 </general> | 43 </general> |
43 <individual> | 44 <individual> |
45 <category name="General" label="%(category_general)s"> | |
46 <param name="Password" value="" type="password" /> | |
47 </category> | |
44 <category name="Connection" label="%(category_connection)s"> | 48 <category name="Connection" label="%(category_connection)s"> |
45 <param name="JabberID" value="name@example.org/SàT" type="string" /> | 49 <param name="JabberID" value="name@example.org/SàT" type="string" /> |
46 <param name="Password" value="" type="password" /> | 50 <param name="Password" value="" type="password" /> |
47 <param name="Priority" value="50" type="string" /> | 51 <param name="Priority" value="50" type="string" /> |
48 <param name="Server" value="example.org" type="string" /> | 52 <param name="Server" value="example.org" type="string" /> |
55 <param name="Watched" value="test@Jabber.goffi.int" type="string" /> | 59 <param name="Watched" value="test@Jabber.goffi.int" type="string" /> |
56 </category> | 60 </category> |
57 </individual> | 61 </individual> |
58 </params> | 62 </params> |
59 """ % { | 63 """ % { |
64 'category_general': _("General"), | |
60 'category_connection': _("Connection"), | 65 'category_connection': _("Connection"), |
61 'label_NewAccount': _("Register new account"), | 66 'label_NewAccount': _("Register new account"), |
62 'label_autoconnect': _('Connect on frontend startup'), | 67 'label_autoconnect': _('Connect on frontend startup'), |
63 'label_autodisconnect': _('Disconnect on frontend closure'), | 68 'label_autodisconnect': _('Disconnect on frontend closure'), |
64 'category_misc': _("Misc") | 69 'category_misc': _("Misc") |
309 log.debug ("Default value to set, using callback") | 314 log.debug ("Default value to set, using callback") |
310 d = defer.maybeDeferred(callback) | 315 d = defer.maybeDeferred(callback) |
311 d.addCallback(self.__default_ok, name, category) | 316 d.addCallback(self.__default_ok, name, category) |
312 d.addErrback(errback or self.__default_ko, name, category) | 317 d.addErrback(errback or self.__default_ko, name, category) |
313 | 318 |
314 def _getAttr(self, node, attr, value): | 319 def _getAttr_internal(self, node, attr, value): |
315 """ get attribute value | 320 """Get attribute value. |
321 | |
322 /!\ This method would return encrypted password values. | |
323 | |
316 @param node: XML param node | 324 @param node: XML param node |
317 @param attr: name of the attribute to get (e.g.: 'value' or 'type') | 325 @param attr: name of the attribute to get (e.g.: 'value' or 'type') |
318 @param value: user defined value""" | 326 @param value: user defined value |
327 @return: str | |
328 """ | |
319 if attr == 'value': | 329 if attr == 'value': |
320 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 | 330 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 |
321 if node.getAttribute('type') == 'bool': | 331 if node.getAttribute('type') == 'bool': |
322 return value_to_use.lower() not in ('false', '0', 'no') | 332 return value_to_use.lower() not in ('false', '0', 'no') |
323 return value_to_use | 333 return value_to_use |
324 return node.getAttribute(attr) | 334 return node.getAttribute(attr) |
325 | 335 |
336 def _getAttr(self, node, attr, value): | |
337 """Get attribute value (synchronous). | |
338 | |
339 /!\ This method can not be used to retrieve password values. | |
340 | |
341 @param node: XML param node | |
342 @param attr: name of the attribute to get (e.g.: 'value' or 'type') | |
343 @param value: user defined value | |
344 @return: str | |
345 """ | |
346 if attr == 'value' and node.getAttribute('type') == 'password': | |
347 raise exceptions.InternalError('To retrieve password values, use _asyncGetAttr instead of _getAttr') | |
348 return self._getAttr_internal(node, attr, value) | |
349 | |
350 def _asyncGetAttr(self, node, attr, value, profile=None): | |
351 """Get attribute value. | |
352 | |
353 Profile passwords are returned hashed (if not empty), | |
354 other passwords are returned decrypted (if not empty). | |
355 | |
356 @param node: XML param node | |
357 @param attr: name of the attribute to get (e.g.: 'value' or 'type') | |
358 @param value: user defined value | |
359 @param profile: %(doc_profile)s | |
360 @return: a deferred str | |
361 """ | |
362 value = self._getAttr_internal(node, attr, value) | |
363 if attr != 'value' or node.getAttribute('type') != 'password': | |
364 return defer.succeed(value) | |
365 param_cat = node.parentNode.getAttribute('name') | |
366 param_name = node.getAttribute('name') | |
367 if ((param_cat, param_name) == C.PROFILE_PASS_PATH) or not value: | |
368 return defer.succeed(value) # profile password and empty passwords are returned "as is" | |
369 if not profile: | |
370 raise exceptions.ProfileUnknownError('The profile is needed to decrypt a password') | |
371 try: | |
372 personal_key = self.host.memory.auth_sessions.profileGetUnique(profile)[C.MEMORY_CRYPTO_KEY] | |
373 except TypeError: | |
374 raise exceptions.InternalError(_('Trying to decrypt a password while the personal key is undefined!')) | |
375 d = BlockCipher.decrypt(personal_key, value) | |
376 | |
377 def gotPlainPassword(password): | |
378 if password is None: # empty value means empty password, None means decryption failure | |
379 raise exceptions.InternalError(_('The stored password could not be decrypted!')) | |
380 return password | |
381 | |
382 return d.addCallback(gotPlainPassword) | |
383 | |
326 def __type_to_string(self, result): | 384 def __type_to_string(self, result): |
327 """ convert result to string, according to its type """ | 385 """ convert result to string, according to its type """ |
328 if isinstance(result, bool): | 386 if isinstance(result, bool): |
329 return "true" if result else "false" | 387 return "true" if result else "false" |
330 return result | 388 return result |
332 def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): | 390 def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): |
333 """ Same as getParamA but for bridge: convert non string value to string """ | 391 """ Same as getParamA but for bridge: convert non string value to string """ |
334 return self.__type_to_string(self.getParamA(name, category, attr, profile_key)) | 392 return self.__type_to_string(self.getParamA(name, category, attr, profile_key)) |
335 | 393 |
336 def getParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): | 394 def getParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): |
337 """Helper method to get a specific attribute | 395 """Helper method to get a specific attribute. |
338 @param name: name of the parameter | 396 |
339 @param category: category of the parameter | 397 /!\ This method would return encrypted password values, |
340 @param attr: name of the attribute (default: "value") | 398 to get the plain values you have to use _asyncGetParamA. |
341 @param profile: owner of the param (@ALL@ for everyone) | 399 |
342 | 400 @param name: name of the parameter |
343 @return: attribute""" | 401 @param category: category of the parameter |
402 @param attr: name of the attribute (default: "value") | |
403 @param profile: owner of the param (@ALL@ for everyone) | |
404 @return: attribute | |
405 """ | |
344 #FIXME: looks really dirty and buggy, need to be reviewed/refactored | 406 #FIXME: looks really dirty and buggy, need to be reviewed/refactored |
345 node = self._getParamNode(name, category) | 407 node = self._getParamNode(name, category) |
346 if not node: | 408 if not node: |
347 log.error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category}) | 409 log.error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category}) |
348 raise exceptions.NotFound | 410 raise exceptions.NotFound |
349 | 411 |
412 if attr == 'value' and node[1].getAttribute('type') == 'password': | |
413 raise exceptions.InternalError('To retrieve password values, use asyncGetParamA instead of getParamA') | |
414 | |
350 if node[0] == C.GENERAL: | 415 if node[0] == C.GENERAL: |
351 value = self._getParam(category, name, C.GENERAL) | 416 value = self._getParam(category, name, C.GENERAL) |
352 return self._getAttr(node[1], attr, value) | 417 return self._getAttr(node[1], attr, value) |
353 | 418 |
354 assert node[0] == C.INDIVIDUAL | 419 assert node[0] == C.INDIVIDUAL |
370 d = self.asyncGetParamA(name, category, attr, security_limit, profile_key) | 435 d = self.asyncGetParamA(name, category, attr, security_limit, profile_key) |
371 d.addCallback(self.__type_to_string) | 436 d.addCallback(self.__type_to_string) |
372 return d | 437 return d |
373 | 438 |
374 def asyncGetParamA(self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE): | 439 def asyncGetParamA(self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE): |
375 """Helper method to get a specific attribute | 440 """Helper method to get a specific attribute. |
376 @param name: name of the parameter | 441 @param name: name of the parameter |
377 @param category: category of the parameter | 442 @param category: category of the parameter |
378 @param attr: name of the attribute (default: "value") | 443 @param attr: name of the attribute (default: "value") |
379 @param profile: owner of the param (@ALL@ for everyone)""" | 444 @param profile: owner of the param (@ALL@ for everyone) |
445 @return: Deferred | |
446 """ | |
380 node = self._getParamNode(name, category) | 447 node = self._getParamNode(name, category) |
381 if not node: | 448 if not node: |
382 log.error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category}) | 449 log.error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category}) |
383 return None | 450 return defer.succeed(None) |
384 | 451 |
385 if not self.checkSecurityLimit(node[1], security_limit): | 452 if not self.checkSecurityLimit(node[1], security_limit): |
386 log.warning(_("Trying to get parameter '%(param)s' in category '%(cat)s' without authorization!!!" | 453 log.warning(_("Trying to get parameter '%(param)s' in category '%(cat)s' without authorization!!!" |
387 % {'param': name, 'cat': category})) | 454 % {'param': name, 'cat': category})) |
388 return None | 455 return defer.succeed(None) |
389 | 456 |
390 if node[0] == C.GENERAL: | 457 if node[0] == C.GENERAL: |
391 value = self._getParam(category, name, C.GENERAL) | 458 value = self._getParam(category, name, C.GENERAL) |
392 return defer.succeed(self._getAttr(node[1], attr, value)) | 459 return self._asyncGetAttr(node[1], attr, value) |
393 | 460 |
394 assert node[0] == C.INDIVIDUAL | 461 assert node[0] == C.INDIVIDUAL |
395 | 462 |
396 profile = self.getProfileName(profile_key) | 463 profile = self.getProfileName(profile_key) |
397 if not profile: | 464 if not profile: |
398 log.error(_('Requesting a param for a non-existant profile')) | 465 raise exceptions.InternalError(_('Requesting a param for a non-existant profile')) |
399 return defer.fail() | |
400 | 466 |
401 if attr != "value": | 467 if attr != "value": |
402 return defer.succeed(node[1].getAttribute(attr)) | 468 return defer.succeed(node[1].getAttribute(attr)) |
403 try: | 469 try: |
404 value = self._getParam(category, name, profile=profile) | 470 value = self._getParam(category, name, profile=profile) |
405 return defer.succeed(self._getAttr(node[1], attr, value)) | 471 return self._asyncGetAttr(node[1], attr, value, profile) |
406 except exceptions.ProfileNotInCacheError: | 472 except exceptions.ProfileNotInCacheError: |
407 #We have to ask data to the storage manager | 473 #We have to ask data to the storage manager |
408 d = self.storage.getIndParam(category, name, profile) | 474 d = self.storage.getIndParam(category, name, profile) |
409 return d.addCallback(lambda value: self._getAttr(node[1], attr, value)) | 475 return d.addCallback(lambda value: self._asyncGetAttr(node[1], attr, value, profile)) |
410 | 476 |
411 def _getParam(self, category, name, type_=C.INDIVIDUAL, cache=None, profile=C.PROF_KEY_NONE): | 477 def _getParam(self, category, name, type_=C.INDIVIDUAL, cache=None, profile=C.PROF_KEY_NONE): |
412 """Return the param, or None if it doesn't exist | 478 """Return the param, or None if it doesn't exist |
413 @param category: param category | 479 @param category: param category |
414 @param name: param name | 480 @param name: param name |
623 if name not in categories: | 689 if name not in categories: |
624 categories.append(cat.getAttribute("name")) | 690 categories.append(cat.getAttribute("name")) |
625 return categories | 691 return categories |
626 | 692 |
627 def setParam(self, name, value, category, security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE): | 693 def setParam(self, name, value, category, security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE): |
628 """Set a parameter, return None if the parameter is not in param xml""" | 694 """Set a parameter, return None if the parameter is not in param xml. |
629 #TODO: use different behaviour depending of the data type (e.g. password encrypted) | 695 |
696 Parameter of type 'password' that are not the SàT profile password are | |
697 stored encrypted (if not empty). The profile password is stored hashed | |
698 (if not empty). | |
699 | |
700 @param name (str): the parameter name | |
701 @param value (str): the new value | |
702 @param category (str): the parameter category | |
703 @param security_limit (int) | |
704 @param profile_key (str): %(doc_profile_key)s | |
705 @return: a deferred None value when everything is done | |
706 """ | |
630 if profile_key != C.PROF_KEY_NONE: | 707 if profile_key != C.PROF_KEY_NONE: |
631 profile = self.getProfileName(profile_key) | 708 profile = self.getProfileName(profile_key) |
632 if not profile: | 709 if not profile: |
633 log.error(_('Trying to set parameter for an unknown profile')) | 710 log.error(_('Trying to set parameter for an unknown profile')) |
634 return # TODO: throw an error | 711 raise exceptions.ProfileUnknownError |
635 | 712 |
636 node = self._getParamNode(name, category, '@ALL@') | 713 node = self._getParamNode(name, category, '@ALL@') |
637 if not node: | 714 if not node: |
638 log.error(_('Requesting an unknown parameter (%(category)s/%(name)s)') | 715 log.error(_('Requesting an unknown parameter (%(category)s/%(name)s)') |
639 % {'category': category, 'name': name}) | 716 % {'category': category, 'name': name}) |
640 return | 717 return defer.succeed(None) |
641 | 718 |
642 if not self.checkSecurityLimit(node[1], security_limit): | 719 if not self.checkSecurityLimit(node[1], security_limit): |
643 log.warning(_("Trying to set parameter '%(param)s' in category '%(cat)s' without authorization!!!" | 720 log.warning(_("Trying to set parameter '%(param)s' in category '%(cat)s' without authorization!!!" |
644 % {'param': name, 'cat': category})) | 721 % {'param': name, 'cat': category})) |
645 return | 722 return defer.succeed(None) |
723 | |
724 type_ = node[1].getAttribute("type") | |
725 log.info(_("Setting parameter (%(category)s, %(name)s) = %(value)s") % | |
726 {'category': category, 'name': name, 'value': value if type_ != 'password' else '********'}) | |
646 | 727 |
647 if node[0] == C.GENERAL: | 728 if node[0] == C.GENERAL: |
648 self.params_gen[(category, name)] = value | 729 self.params_gen[(category, name)] = value |
649 self.storage.setGenParam(category, name, value) | 730 self.storage.setGenParam(category, name, value) |
650 for profile in self.storage.getProfilesList(): | 731 for profile in self.storage.getProfilesList(): |
651 if self.host.isConnected(profile): | 732 if self.host.isConnected(profile): |
652 self.host.bridge.paramUpdate(name, value, category, profile) | 733 self.host.bridge.paramUpdate(name, value, category, profile) |
653 self.host.trigger.point("paramUpdateTrigger", name, value, category, node[0], profile) | 734 self.host.trigger.point("paramUpdateTrigger", name, value, category, node[0], profile) |
654 return | 735 return defer.succeed(None) |
655 | 736 |
656 assert (node[0] == C.INDIVIDUAL) | 737 assert (node[0] == C.INDIVIDUAL) |
657 assert (profile_key != C.PROF_KEY_NONE) | 738 assert (profile_key != C.PROF_KEY_NONE) |
658 | 739 |
659 type_ = node[1].getAttribute("type") | 740 d_list = [] |
660 if type_ == "button": | 741 if type_ == "button": |
661 print "clique", node.toxml() | 742 log.debug("Clicked param button %s" % node.toxml()) |
743 return defer.succeed(None) | |
744 elif type_ == "password": | |
745 try: | |
746 personal_key = self.host.memory.auth_sessions.profileGetUnique(profile)[C.MEMORY_CRYPTO_KEY] | |
747 except TypeError: | |
748 raise exceptions.InternalError(_('Trying to encrypt a password while the personal key is undefined!')) | |
749 if (category, name) == C.PROFILE_PASS_PATH: | |
750 # using 'value' as the encryption key to encrypt another encryption key... could be confusing! | |
751 d_list.append(self.host.memory.encryptPersonalData(data_key=C.MEMORY_CRYPTO_KEY, | |
752 data_value=personal_key, | |
753 crypto_key=value, | |
754 profile=profile)) | |
755 d = PasswordHasher.hash(value) # profile password is hashed (empty value stays empty) | |
756 elif value: # other non empty passwords are encrypted with the personal key | |
757 d = BlockCipher.encrypt(personal_key, value) | |
662 else: | 758 else: |
759 d = defer.succeed(value) | |
760 | |
761 def gotFinalValue(value): | |
663 if self.host.isConnected(profile): # key can not exists if profile is not connected | 762 if self.host.isConnected(profile): # key can not exists if profile is not connected |
664 self.params[profile][(category, name)] = value | 763 self.params[profile][(category, name)] = value |
665 self.host.bridge.paramUpdate(name, value, category, profile) | 764 self.host.bridge.paramUpdate(name, value, category, profile) |
666 self.host.trigger.point("paramUpdateTrigger", name, value, category, node[0], profile) | 765 self.host.trigger.point("paramUpdateTrigger", name, value, category, node[0], profile) |
667 self.storage.setIndParam(category, name, value, profile) | 766 d_list.append(self.storage.setIndParam(category, name, value, profile)) |
767 | |
768 d.addCallback(gotFinalValue) | |
769 return defer.DeferredList(d_list).addCallback(lambda dummy: None) | |
770 | |
771 def _getNodesOfTypes(self, attr_type, node_type="@ALL@"): | |
772 """Return all the nodes matching the given types. | |
773 | |
774 TODO: using during the dev but not anymore... remove if not needed | |
775 | |
776 @param attr_type (str): the attribute type (string, text, password, bool, button, list) | |
777 @param node_type (str): keyword for filtering: | |
778 @ALL@ search everywhere | |
779 @GENERAL@ only search in general type | |
780 @INDIVIDUAL@ only search in individual type | |
781 @return: dict{tuple: node}: a dict {key, value} where: | |
782 - key is a couple (attribute category, attribute name) | |
783 - value is a node | |
784 """ | |
785 ret = {} | |
786 for type_node in self.dom.documentElement.childNodes: | |
787 if (((node_type == "@ALL@" or node_type == "@GENERAL@") and type_node.nodeName == C.GENERAL) or | |
788 ((node_type == "@ALL@" or node_type == "@INDIVIDUAL@") and type_node.nodeName == C.INDIVIDUAL)): | |
789 for cat_node in type_node.getElementsByTagName('category'): | |
790 cat = cat_node.getAttribute('name') | |
791 params = cat_node.getElementsByTagName("param") | |
792 for param in params: | |
793 if param.getAttribute("type") == attr_type: | |
794 ret[(cat, param.getAttribute("name"))] = param | |
795 return ret | |
668 | 796 |
669 def checkSecurityLimit(self, node, security_limit): | 797 def checkSecurityLimit(self, node, security_limit): |
670 """Check the given node against the given security limit. | 798 """Check the given node against the given security limit. |
671 The value NO_SECURITY_LIMIT (-1) means that everything is allowed. | 799 The value NO_SECURITY_LIMIT (-1) means that everything is allowed. |
672 @return: True if this node can be accessed with the given security limit. | 800 @return: True if this node can be accessed with the given security limit. |