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
This commit is contained in:
souliane 2014-05-07 16:02:23 +02:00
commit 0d28c03d27

View file

@ -21,6 +21,7 @@ from sat.core.i18n import _
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 @@ class Params(object):
<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 @@ class Params(object):
</individual>
</params>
""" % {
'category_general': _("General"),
'category_connection': _("Connection"),
'label_NewAccount': _("Register new account"),
'label_autoconnect': _('Connect on frontend startup'),
@ -311,11 +316,16 @@ class Params(object):
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 @@ class Params(object):
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 @@ class Params(object):
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.
@return: attribute"""
/!\ This method would return encrypted password values,
to get the plain values you have to use _asyncGetParamA.
@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 @@ class Params(object):
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 @@ class Params(object):
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 @@ class Params(object):
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.