Mercurial > libervia-backend
diff src/memory/sqlite.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 | 6a16ec17a458 |
children | 65fffdcb97f1 |
line wrap: on
line diff
--- a/src/memory/sqlite.py Sat May 10 17:37:32 2014 +0200 +++ b/src/memory/sqlite.py Wed May 07 16:02:23 2014 +0200 @@ -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 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 @@ # 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 @@ ("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 @@ ('message_types', (("'chat'",), ("'error'",), ("'groupchat'",), ("'headline'",), ("'normal'",))), )), }, + 2: {'specific': 'update2raw_v2' + }, 1: {'cols create': {'history': ('extra BLOB',)} }, } @@ -508,7 +511,7 @@ @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 @@ 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 @@ # 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 @@ '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 @@ 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