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.