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:
parent
c9f4397000
commit
0d28c03d27
1 changed files with 478 additions and 166 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue