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
3 changed files with 478 additions and 166 deletions
|
|
@ -34,6 +34,7 @@ from sat.memory.sqlite import SqliteStorage
|
|||
from sat.memory.persistent import PersistentDict
|
||||
from sat.memory.params import Params
|
||||
from sat.memory.disco import Discovery
|
||||
from sat.memory.crypto import BlockCipher
|
||||
|
||||
|
||||
class Sessions(object):
|
||||
|
|
@ -210,6 +211,7 @@ class Memory(object):
|
|||
self._entities_cache = {} # XXX: keep presence/last resource/other data in cache
|
||||
# /!\ an entity is not necessarily in roster
|
||||
self.subscriptions = {}
|
||||
self.auth_sessions = ProfileSessions() # remember the authenticated profiles
|
||||
self.disco = Discovery(host)
|
||||
fixLocalDir(False) # XXX: tmp update code, will be removed in the future
|
||||
self.config = self.parseMainConf()
|
||||
|
|
@ -259,7 +261,7 @@ class Memory(object):
|
|||
"""Load parameters template from xml file
|
||||
|
||||
@param filename (str): input file
|
||||
@return bool: True in case of success
|
||||
@return: bool: True in case of success
|
||||
"""
|
||||
if not filename:
|
||||
return False
|
||||
|
|
@ -289,6 +291,24 @@ class Memory(object):
|
|||
log.info(_("[%s] Profile session started" % profile))
|
||||
self._entities_cache[profile] = {}
|
||||
|
||||
def newAuthSession(self, key, profile):
|
||||
"""Start a new session for the authenticated profile.
|
||||
|
||||
The personal key is loaded encrypted from a PersistentDict before being decrypted.
|
||||
|
||||
@param key: the key to decrypt the personal key
|
||||
@param profile: %(doc_profile)s
|
||||
@return: a deferred None value
|
||||
"""
|
||||
def gotPersonalKey(personal_key):
|
||||
"""Create the session for this profile and store the personal key"""
|
||||
self.auth_sessions.newSession({C.MEMORY_CRYPTO_KEY: personal_key}, profile)
|
||||
log.debug('auth session created for profile %s' % profile)
|
||||
|
||||
d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
|
||||
d.addCallback(lambda data: BlockCipher.decrypt(key, data[C.MEMORY_CRYPTO_KEY]))
|
||||
return d.addCallback(gotPersonalKey)
|
||||
|
||||
def purgeProfileSession(self, profile):
|
||||
"""Delete cache of data of profile
|
||||
@param profile: %(doc_profile)s"""
|
||||
|
|
@ -303,7 +323,7 @@ class Memory(object):
|
|||
"""Save parameters template to xml file
|
||||
|
||||
@param filename (str): output file
|
||||
@return bool: True in case of success
|
||||
@return: bool: True in case of success
|
||||
"""
|
||||
if not filename:
|
||||
return False
|
||||
|
|
@ -320,17 +340,23 @@ class Memory(object):
|
|||
def getProfilesList(self):
|
||||
return self.storage.getProfilesList()
|
||||
|
||||
def getProfileName(self, profile_key, return_profile_keys = False):
|
||||
def getProfileName(self, profile_key, return_profile_keys=False):
|
||||
"""Return name of profile from keyword
|
||||
@param profile_key: can be the profile name or a keywork (like @DEFAULT@)
|
||||
@return: profile name or None if it doesn't exist"""
|
||||
return self.params.getProfileName(profile_key, return_profile_keys)
|
||||
|
||||
def asyncCreateProfile(self, name):
|
||||
def asyncCreateProfile(self, name, password=''):
|
||||
"""Create a new profile
|
||||
@param name: Profile name
|
||||
@param name: profile name
|
||||
@param password: profile password
|
||||
@return: Deferred
|
||||
"""
|
||||
return self.params.asyncCreateProfile(name)
|
||||
personal_key = BlockCipher.getRandomKey(base64=True) # generated once for all and saved in a PersistentDict
|
||||
self.auth_sessions.newSession({C.MEMORY_CRYPTO_KEY: personal_key}, name) # will be encrypted by setParam
|
||||
d = self.params.asyncCreateProfile(name)
|
||||
d.addCallback(lambda dummy: self.setParam(C.PROFILE_PASS_PATH[1], password, C.PROFILE_PASS_PATH[0], profile_key=name))
|
||||
return d
|
||||
|
||||
def asyncDeleteProfile(self, name, force=False):
|
||||
"""Delete an existing profile
|
||||
|
|
@ -355,7 +381,6 @@ class Memory(object):
|
|||
jid_ = jid.JID(jid_s)
|
||||
return self.getLastResource(jid_, profile_key) or ""
|
||||
|
||||
|
||||
def getLastResource(self, entity_jid, profile_key):
|
||||
"""Return the last resource used by an entity
|
||||
@param entity_jid: entity jid
|
||||
|
|
@ -502,7 +527,6 @@ class Memory(object):
|
|||
"""
|
||||
return self.getEntityData(entity_jid, (key,), profile_key)[key]
|
||||
|
||||
|
||||
def delEntityCache(self, entity_jid, delete_all_resources=True, profile_key=C.PROF_KEY_NONE):
|
||||
"""Remove cached data for entity
|
||||
@param entity_jid: JID of the entity to delete
|
||||
|
|
@ -527,6 +551,32 @@ class Memory(object):
|
|||
except KeyError:
|
||||
log.debug("Can't delete entity [%s]: not in cache" % entity.full())
|
||||
|
||||
def encryptPersonalData(self, data_key, data_value, crypto_key, profile):
|
||||
"""Re-encrypt a personal data (saved to a PersistentDict).
|
||||
|
||||
@param data_key: key for the individual PersistentDict instance
|
||||
@param data_value: the value to be encrypted
|
||||
@param crypto_key: the key to encrypt the value
|
||||
@param profile: %(profile_doc)s
|
||||
@return: a deferred None value
|
||||
"""
|
||||
|
||||
def gotIndMemory(data):
|
||||
d = BlockCipher.encrypt(crypto_key, data_value)
|
||||
|
||||
def cb(cipher):
|
||||
data[data_key] = cipher
|
||||
return data.force(data_key)
|
||||
|
||||
return d.addCallback(cb)
|
||||
|
||||
def done(dummy):
|
||||
log.debug(_('Personal data (%(ns)s, %(key)s) has been successfuly encrypted') %
|
||||
{'ns': C.MEMORY_CRYPTO_NAMESPACE, 'key': data_key})
|
||||
|
||||
d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
|
||||
return d.addCallback(gotIndMemory).addCallback(done)
|
||||
|
||||
def addWaitingSub(self, type_, entity_jid, profile_key):
|
||||
"""Called when a subcription request is received"""
|
||||
profile = self.getProfileName(profile_key)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from sat.core.i18n import _
|
||||
from sat.core.constants import Const as C
|
||||
from sat.core import exceptions
|
||||
from sat.core.log import getLogger
|
||||
log = getLogger(__name__)
|
||||
from sat.memory.crypto import BlockCipher, PasswordHasher
|
||||
from twisted.enterprise import adbapi
|
||||
from twisted.internet import defer
|
||||
from collections import OrderedDict
|
||||
|
|
@ -30,7 +32,7 @@ import os.path
|
|||
import cPickle as pickle
|
||||
import hashlib
|
||||
|
||||
CURRENT_DB_VERSION = 1
|
||||
CURRENT_DB_VERSION = 2
|
||||
|
||||
# XXX: DATABASE schemas are used in the following way:
|
||||
# - 'current' key is for the actual database schema, for a new base
|
||||
|
|
@ -39,7 +41,7 @@ CURRENT_DB_VERSION = 1
|
|||
# a 'current' data dict can contains the keys:
|
||||
# - 'CREATE': it contains an Ordered dict with table to create as keys, and a len 2 tuple as value, where value[0] are the columns definitions and value[1] are the table constraints
|
||||
# - 'INSERT': it contains an Ordered dict with table where values have to be inserted, and many tuples containing values to insert in the order of the rows (#TODO: manage named columns)
|
||||
# an update data dict (the ones with a number) can contains the keys 'create', 'delete', 'cols create', 'cols delete', 'cols modify' or 'insert'. See Updater.generateUpdateData for more infos. This metho can be used to autogenerate update_data, to ease the work of the developers.
|
||||
# an update data dict (the ones with a number) can contains the keys 'create', 'delete', 'cols create', 'cols delete', 'cols modify', 'insert' or 'specific'. See Updater.generateUpdateData for more infos. This method can be used to autogenerate update_data, to ease the work of the developers.
|
||||
|
||||
DATABASE_SCHEMAS = {
|
||||
"current": {'CREATE': OrderedDict((
|
||||
|
|
@ -59,7 +61,6 @@ DATABASE_SCHEMAS = {
|
|||
("PRIMARY KEY (namespace, key, profile_id)", "FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE"))),
|
||||
('private_gen_bin', (("namespace TEXT", "key TEXT", "value BLOB"),
|
||||
("PRIMARY KEY (namespace, key)",))),
|
||||
|
||||
('private_ind_bin', (("namespace TEXT", "key TEXT", "profile_id INTEGER", "value BLOB"),
|
||||
("PRIMARY KEY (namespace, key, profile_id)", "FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE")))
|
||||
)),
|
||||
|
|
@ -67,6 +68,8 @@ DATABASE_SCHEMAS = {
|
|||
('message_types', (("'chat'",), ("'error'",), ("'groupchat'",), ("'headline'",), ("'normal'",))),
|
||||
)),
|
||||
},
|
||||
2: {'specific': 'update2raw_v2'
|
||||
},
|
||||
1: {'cols create': {'history': ('extra BLOB',)}
|
||||
},
|
||||
}
|
||||
|
|
@ -508,7 +511,7 @@ class Updater(object):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def checkUpdates(self):
|
||||
""" Check is database schema update is needed, according to DATABASE_SCHEMAS
|
||||
""" Check if a database schema/content update is needed, according to DATABASE_SCHEMAS
|
||||
@return: deferred which fire a list of SQL update statements, or None if no update is needed
|
||||
|
||||
"""
|
||||
|
|
@ -519,7 +522,11 @@ class Updater(object):
|
|||
local_hash = self.statementHash(local_sch)
|
||||
current_hash = self.statementHash(current_sch)
|
||||
|
||||
if local_hash == current_hash:
|
||||
# Force the update if the schemas are unchanged but a specific update is needed
|
||||
force_update = local_hash == current_hash and local_version < CURRENT_DB_VERSION \
|
||||
and 'specific' in DATABASE_SCHEMAS[CURRENT_DB_VERSION]
|
||||
|
||||
if local_hash == current_hash and not force_update:
|
||||
if local_version != CURRENT_DB_VERSION:
|
||||
log.warning(_("Your local schema is up-to-date, but database versions mismatch, fixing it..."))
|
||||
yield self._setLocalVersion(CURRENT_DB_VERSION)
|
||||
|
|
@ -532,23 +539,28 @@ class Updater(object):
|
|||
# we are in a development version
|
||||
update_data = self.generateUpdateData(local_sch, current_sch, False)
|
||||
log.warning(_("There is a schema mismatch, but as we are on a dev version, database will be updated"))
|
||||
update_raw = self.update2raw(update_data, True)
|
||||
update_raw = yield self.update2raw(update_data, True)
|
||||
defer.returnValue(update_raw)
|
||||
else:
|
||||
log.error(_(u"schema version is up-to-date, but local schema differ from expected current schema"))
|
||||
update_data = self.generateUpdateData(local_sch, current_sch, True)
|
||||
log.warning(_(u"Here are the commands that should fix the situation, use at your own risk (do a backup before modifying database), you can go to SàT's MUC room at sat@chat.jabberfr.org for help\n### SQL###\n%s\n### END SQL ###\n") % u'\n'.join(("%s;" % statement for statement in self.update2raw(update_data))))
|
||||
update_raw = yield self.update2raw(update_data)
|
||||
log.warning(_(u"Here are the commands that should fix the situation, use at your own risk (do a backup before modifying database), you can go to SàT's MUC room at sat@chat.jabberfr.org for help\n### SQL###\n%s\n### END SQL ###\n") % u'\n'.join("%s;" % statement for statement in update_raw))
|
||||
raise exceptions.DatabaseError("Database mismatch")
|
||||
else:
|
||||
# Database is not up-to-date, we'll do the update
|
||||
log.info(_("Database schema has changed, local database will be updated"))
|
||||
if force_update:
|
||||
log.info(_("Database content needs a specific processing, local database will be updated"))
|
||||
else:
|
||||
log.info(_("Database schema has changed, local database will be updated"))
|
||||
update_raw = []
|
||||
for version in xrange(local_version + 1, CURRENT_DB_VERSION + 1):
|
||||
try:
|
||||
update_data = DATABASE_SCHEMAS[version]
|
||||
except KeyError:
|
||||
raise exceptions.InternalError("Missing update definition (version %d)" % version)
|
||||
update_raw.extend(self.update2raw(update_data))
|
||||
update_raw_step = yield self.update2raw(update_data)
|
||||
update_raw.extend(update_raw_step)
|
||||
update_raw.append("PRAGMA user_version=%d" % CURRENT_DB_VERSION)
|
||||
defer.returnValue(update_raw)
|
||||
|
||||
|
|
@ -693,6 +705,7 @@ class Updater(object):
|
|||
'cols modify': modify_cols_data
|
||||
}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def update2raw(self, update, dev_version=False):
|
||||
""" Transform update data to raw SQLite statements
|
||||
@param upadte: update data as returned by generateUpdateData
|
||||
|
|
@ -731,4 +744,60 @@ class Updater(object):
|
|||
insert = update.get('insert', {})
|
||||
ret.extend(self.insertData2Raw(insert))
|
||||
|
||||
return ret
|
||||
specific = update.get('specific', None)
|
||||
if specific:
|
||||
cmds = yield getattr(self, specific)()
|
||||
ret.extend(cmds)
|
||||
defer.returnValue(ret)
|
||||
|
||||
def update2raw_v2(self):
|
||||
"""Update the database from v1 to v2 (add passwords encryptions):
|
||||
- the XMPP password value is re-used for the profile password (new parameter)
|
||||
- the profile password is stored hashed
|
||||
- the XMPP password is stored encrypted, with the profile password as key
|
||||
- as there are no other stored passwords yet, it is enough, otherwise we
|
||||
would need to encrypt the other passwords as it's done for XMPP password
|
||||
"""
|
||||
xmpp_pass_path = ('Connection', 'Password')
|
||||
|
||||
def encrypt_values(values):
|
||||
ret = []
|
||||
list_ = []
|
||||
for profile_id, xmpp_password in values:
|
||||
def prepare_queries(result):
|
||||
try:
|
||||
id_ = result[0][0]
|
||||
except IndexError:
|
||||
log.error("Profile of id %d is referenced in 'param_ind' but it doesn't exist!" % profile_id)
|
||||
return
|
||||
|
||||
sat_password = xmpp_password
|
||||
d1 = PasswordHasher.hash(sat_password)
|
||||
personal_key = BlockCipher.getRandomKey(base64=True)
|
||||
d2 = BlockCipher.encrypt(sat_password, personal_key)
|
||||
d3 = BlockCipher.encrypt(personal_key, xmpp_password)
|
||||
|
||||
def gotValues(res):
|
||||
sat_cipher, personal_cipher, xmpp_cipher = res[0][1], res[1][1], res[2][1]
|
||||
ret.append("INSERT INTO param_ind(category,name,profile_id,value) VALUES ('%s','%s',%s,'%s')" %
|
||||
(C.PROFILE_PASS_PATH[0], C.PROFILE_PASS_PATH[1], id_, sat_cipher))
|
||||
|
||||
ret.append("INSERT INTO private_ind(namespace,key,profile_id,value) VALUES ('%s','%s',%s,'%s')" %
|
||||
(C.MEMORY_CRYPTO_NAMESPACE, C.MEMORY_CRYPTO_KEY, id_, personal_cipher))
|
||||
|
||||
ret.append("REPLACE INTO param_ind(category,name,profile_id,value) VALUES ('%s','%s',%s,'%s')" %
|
||||
(xmpp_pass_path[0], xmpp_pass_path[1], id_, xmpp_cipher))
|
||||
|
||||
return defer.DeferredList([d1, d2, d3]).addCallback(gotValues)
|
||||
|
||||
d = self.dbpool.runQuery("SELECT id FROM profiles WHERE id=?", (profile_id,))
|
||||
d.addCallback(prepare_queries)
|
||||
list_.append(d)
|
||||
|
||||
d_list = defer.DeferredList(list_)
|
||||
d_list.addCallback(lambda dummy: ret)
|
||||
return d_list
|
||||
|
||||
d = self.dbpool.runQuery("SELECT profile_id,value FROM param_ind WHERE category=? AND name=?", xmpp_pass_path)
|
||||
d.addCallback(encrypt_values)
|
||||
return d
|
||||
|
|
|
|||
Loading…
Reference in a new issue