Mercurial > libervia-backend
comparison 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 |
comparison
equal
deleted
inserted
replaced
1029:f6182f6418ea | 1030:15f43b54d697 |
---|---|
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 17 # You should have received a copy of the GNU Affero General Public License |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 from sat.core.i18n import _ | 20 from sat.core.i18n import _ |
21 from sat.core.constants import Const as C | |
21 from sat.core import exceptions | 22 from sat.core import exceptions |
22 from sat.core.log import getLogger | 23 from sat.core.log import getLogger |
23 log = getLogger(__name__) | 24 log = getLogger(__name__) |
25 from sat.memory.crypto import BlockCipher, PasswordHasher | |
24 from twisted.enterprise import adbapi | 26 from twisted.enterprise import adbapi |
25 from twisted.internet import defer | 27 from twisted.internet import defer |
26 from collections import OrderedDict | 28 from collections import OrderedDict |
27 from time import time | 29 from time import time |
28 import re | 30 import re |
29 import os.path | 31 import os.path |
30 import cPickle as pickle | 32 import cPickle as pickle |
31 import hashlib | 33 import hashlib |
32 | 34 |
33 CURRENT_DB_VERSION = 1 | 35 CURRENT_DB_VERSION = 2 |
34 | 36 |
35 # XXX: DATABASE schemas are used in the following way: | 37 # XXX: DATABASE schemas are used in the following way: |
36 # - 'current' key is for the actual database schema, for a new base | 38 # - 'current' key is for the actual database schema, for a new base |
37 # - x(int) is for update needed between x-1 and x. All number are needed between y and z to do an update | 39 # - x(int) is for update needed between x-1 and x. All number are needed between y and z to do an update |
38 # e.g.: if CURRENT_DB_VERSION is 6, 'current' is the actuel DB, and to update from version 3, numbers 4, 5 and 6 are needed | 40 # e.g.: if CURRENT_DB_VERSION is 6, 'current' is the actuel DB, and to update from version 3, numbers 4, 5 and 6 are needed |
39 # a 'current' data dict can contains the keys: | 41 # a 'current' data dict can contains the keys: |
40 # - '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 | 42 # - '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 |
41 # - '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) | 43 # - '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) |
42 # 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. | 44 # 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. |
43 | 45 |
44 DATABASE_SCHEMAS = { | 46 DATABASE_SCHEMAS = { |
45 "current": {'CREATE': OrderedDict(( | 47 "current": {'CREATE': OrderedDict(( |
46 ('profiles', (("id INTEGER PRIMARY KEY ASC", "name TEXT"), | 48 ('profiles', (("id INTEGER PRIMARY KEY ASC", "name TEXT"), |
47 ("UNIQUE (name)",))), | 49 ("UNIQUE (name)",))), |
57 ("PRIMARY KEY (namespace, key)",))), | 59 ("PRIMARY KEY (namespace, key)",))), |
58 ('private_ind', (("namespace TEXT", "key TEXT", "profile_id INTEGER", "value TEXT"), | 60 ('private_ind', (("namespace TEXT", "key TEXT", "profile_id INTEGER", "value TEXT"), |
59 ("PRIMARY KEY (namespace, key, profile_id)", "FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE"))), | 61 ("PRIMARY KEY (namespace, key, profile_id)", "FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE"))), |
60 ('private_gen_bin', (("namespace TEXT", "key TEXT", "value BLOB"), | 62 ('private_gen_bin', (("namespace TEXT", "key TEXT", "value BLOB"), |
61 ("PRIMARY KEY (namespace, key)",))), | 63 ("PRIMARY KEY (namespace, key)",))), |
62 | |
63 ('private_ind_bin', (("namespace TEXT", "key TEXT", "profile_id INTEGER", "value BLOB"), | 64 ('private_ind_bin', (("namespace TEXT", "key TEXT", "profile_id INTEGER", "value BLOB"), |
64 ("PRIMARY KEY (namespace, key, profile_id)", "FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE"))) | 65 ("PRIMARY KEY (namespace, key, profile_id)", "FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE"))) |
65 )), | 66 )), |
66 'INSERT': OrderedDict(( | 67 'INSERT': OrderedDict(( |
67 ('message_types', (("'chat'",), ("'error'",), ("'groupchat'",), ("'headline'",), ("'normal'",))), | 68 ('message_types', (("'chat'",), ("'error'",), ("'groupchat'",), ("'headline'",), ("'normal'",))), |
68 )), | 69 )), |
69 }, | 70 }, |
71 2: {'specific': 'update2raw_v2' | |
72 }, | |
70 1: {'cols create': {'history': ('extra BLOB',)} | 73 1: {'cols create': {'history': ('extra BLOB',)} |
71 }, | 74 }, |
72 } | 75 } |
73 | 76 |
74 | 77 |
506 d.addCallback(lambda result: [row[0] for row in result]) | 509 d.addCallback(lambda result: [row[0] for row in result]) |
507 return d | 510 return d |
508 | 511 |
509 @defer.inlineCallbacks | 512 @defer.inlineCallbacks |
510 def checkUpdates(self): | 513 def checkUpdates(self): |
511 """ Check is database schema update is needed, according to DATABASE_SCHEMAS | 514 """ Check if a database schema/content update is needed, according to DATABASE_SCHEMAS |
512 @return: deferred which fire a list of SQL update statements, or None if no update is needed | 515 @return: deferred which fire a list of SQL update statements, or None if no update is needed |
513 | 516 |
514 """ | 517 """ |
515 local_version = yield self.getLocalVersion() | 518 local_version = yield self.getLocalVersion() |
516 raw_local_sch = yield self.getLocalSchema() | 519 raw_local_sch = yield self.getLocalSchema() |
517 local_sch = self.rawStatements2data(raw_local_sch) | 520 local_sch = self.rawStatements2data(raw_local_sch) |
518 current_sch = DATABASE_SCHEMAS['current']['CREATE'] | 521 current_sch = DATABASE_SCHEMAS['current']['CREATE'] |
519 local_hash = self.statementHash(local_sch) | 522 local_hash = self.statementHash(local_sch) |
520 current_hash = self.statementHash(current_sch) | 523 current_hash = self.statementHash(current_sch) |
521 | 524 |
522 if local_hash == current_hash: | 525 # Force the update if the schemas are unchanged but a specific update is needed |
526 force_update = local_hash == current_hash and local_version < CURRENT_DB_VERSION \ | |
527 and 'specific' in DATABASE_SCHEMAS[CURRENT_DB_VERSION] | |
528 | |
529 if local_hash == current_hash and not force_update: | |
523 if local_version != CURRENT_DB_VERSION: | 530 if local_version != CURRENT_DB_VERSION: |
524 log.warning(_("Your local schema is up-to-date, but database versions mismatch, fixing it...")) | 531 log.warning(_("Your local schema is up-to-date, but database versions mismatch, fixing it...")) |
525 yield self._setLocalVersion(CURRENT_DB_VERSION) | 532 yield self._setLocalVersion(CURRENT_DB_VERSION) |
526 else: | 533 else: |
527 # an update is needed | 534 # an update is needed |
530 # Database mismatch and we have the latest version | 537 # Database mismatch and we have the latest version |
531 if self._sat_version.endswith('D'): | 538 if self._sat_version.endswith('D'): |
532 # we are in a development version | 539 # we are in a development version |
533 update_data = self.generateUpdateData(local_sch, current_sch, False) | 540 update_data = self.generateUpdateData(local_sch, current_sch, False) |
534 log.warning(_("There is a schema mismatch, but as we are on a dev version, database will be updated")) | 541 log.warning(_("There is a schema mismatch, but as we are on a dev version, database will be updated")) |
535 update_raw = self.update2raw(update_data, True) | 542 update_raw = yield self.update2raw(update_data, True) |
536 defer.returnValue(update_raw) | 543 defer.returnValue(update_raw) |
537 else: | 544 else: |
538 log.error(_(u"schema version is up-to-date, but local schema differ from expected current schema")) | 545 log.error(_(u"schema version is up-to-date, but local schema differ from expected current schema")) |
539 update_data = self.generateUpdateData(local_sch, current_sch, True) | 546 update_data = self.generateUpdateData(local_sch, current_sch, True) |
540 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)))) | 547 update_raw = yield self.update2raw(update_data) |
548 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)) | |
541 raise exceptions.DatabaseError("Database mismatch") | 549 raise exceptions.DatabaseError("Database mismatch") |
542 else: | 550 else: |
543 # Database is not up-to-date, we'll do the update | 551 # Database is not up-to-date, we'll do the update |
544 log.info(_("Database schema has changed, local database will be updated")) | 552 if force_update: |
553 log.info(_("Database content needs a specific processing, local database will be updated")) | |
554 else: | |
555 log.info(_("Database schema has changed, local database will be updated")) | |
545 update_raw = [] | 556 update_raw = [] |
546 for version in xrange(local_version + 1, CURRENT_DB_VERSION + 1): | 557 for version in xrange(local_version + 1, CURRENT_DB_VERSION + 1): |
547 try: | 558 try: |
548 update_data = DATABASE_SCHEMAS[version] | 559 update_data = DATABASE_SCHEMAS[version] |
549 except KeyError: | 560 except KeyError: |
550 raise exceptions.InternalError("Missing update definition (version %d)" % version) | 561 raise exceptions.InternalError("Missing update definition (version %d)" % version) |
551 update_raw.extend(self.update2raw(update_data)) | 562 update_raw_step = yield self.update2raw(update_data) |
563 update_raw.extend(update_raw_step) | |
552 update_raw.append("PRAGMA user_version=%d" % CURRENT_DB_VERSION) | 564 update_raw.append("PRAGMA user_version=%d" % CURRENT_DB_VERSION) |
553 defer.returnValue(update_raw) | 565 defer.returnValue(update_raw) |
554 | 566 |
555 @staticmethod | 567 @staticmethod |
556 def createData2Raw(data): | 568 def createData2Raw(data): |
691 'cols create': create_cols_data, | 703 'cols create': create_cols_data, |
692 'cols delete': delete_cols_data, | 704 'cols delete': delete_cols_data, |
693 'cols modify': modify_cols_data | 705 'cols modify': modify_cols_data |
694 } | 706 } |
695 | 707 |
708 @defer.inlineCallbacks | |
696 def update2raw(self, update, dev_version=False): | 709 def update2raw(self, update, dev_version=False): |
697 """ Transform update data to raw SQLite statements | 710 """ Transform update data to raw SQLite statements |
698 @param upadte: update data as returned by generateUpdateData | 711 @param upadte: update data as returned by generateUpdateData |
699 @param dev_version: if True, update will be done in dev mode: no deletion will be done, instead a message will be shown. This prevent accidental lost of data while working on the code/database. | 712 @param dev_version: if True, update will be done in dev mode: no deletion will be done, instead a message will be shown. This prevent accidental lost of data while working on the code/database. |
700 @return: list of string with SQL statements needed to update the base | 713 @return: list of string with SQL statements needed to update the base |
729 ret.append(self.DROP_SQL % self.TMP_TABLE) | 742 ret.append(self.DROP_SQL % self.TMP_TABLE) |
730 | 743 |
731 insert = update.get('insert', {}) | 744 insert = update.get('insert', {}) |
732 ret.extend(self.insertData2Raw(insert)) | 745 ret.extend(self.insertData2Raw(insert)) |
733 | 746 |
734 return ret | 747 specific = update.get('specific', None) |
748 if specific: | |
749 cmds = yield getattr(self, specific)() | |
750 ret.extend(cmds) | |
751 defer.returnValue(ret) | |
752 | |
753 def update2raw_v2(self): | |
754 """Update the database from v1 to v2 (add passwords encryptions): | |
755 - the XMPP password value is re-used for the profile password (new parameter) | |
756 - the profile password is stored hashed | |
757 - the XMPP password is stored encrypted, with the profile password as key | |
758 - as there are no other stored passwords yet, it is enough, otherwise we | |
759 would need to encrypt the other passwords as it's done for XMPP password | |
760 """ | |
761 xmpp_pass_path = ('Connection', 'Password') | |
762 | |
763 def encrypt_values(values): | |
764 ret = [] | |
765 list_ = [] | |
766 for profile_id, xmpp_password in values: | |
767 def prepare_queries(result): | |
768 try: | |
769 id_ = result[0][0] | |
770 except IndexError: | |
771 log.error("Profile of id %d is referenced in 'param_ind' but it doesn't exist!" % profile_id) | |
772 return | |
773 | |
774 sat_password = xmpp_password | |
775 d1 = PasswordHasher.hash(sat_password) | |
776 personal_key = BlockCipher.getRandomKey(base64=True) | |
777 d2 = BlockCipher.encrypt(sat_password, personal_key) | |
778 d3 = BlockCipher.encrypt(personal_key, xmpp_password) | |
779 | |
780 def gotValues(res): | |
781 sat_cipher, personal_cipher, xmpp_cipher = res[0][1], res[1][1], res[2][1] | |
782 ret.append("INSERT INTO param_ind(category,name,profile_id,value) VALUES ('%s','%s',%s,'%s')" % | |
783 (C.PROFILE_PASS_PATH[0], C.PROFILE_PASS_PATH[1], id_, sat_cipher)) | |
784 | |
785 ret.append("INSERT INTO private_ind(namespace,key,profile_id,value) VALUES ('%s','%s',%s,'%s')" % | |
786 (C.MEMORY_CRYPTO_NAMESPACE, C.MEMORY_CRYPTO_KEY, id_, personal_cipher)) | |
787 | |
788 ret.append("REPLACE INTO param_ind(category,name,profile_id,value) VALUES ('%s','%s',%s,'%s')" % | |
789 (xmpp_pass_path[0], xmpp_pass_path[1], id_, xmpp_cipher)) | |
790 | |
791 return defer.DeferredList([d1, d2, d3]).addCallback(gotValues) | |
792 | |
793 d = self.dbpool.runQuery("SELECT id FROM profiles WHERE id=?", (profile_id,)) | |
794 d.addCallback(prepare_queries) | |
795 list_.append(d) | |
796 | |
797 d_list = defer.DeferredList(list_) | |
798 d_list.addCallback(lambda dummy: ret) | |
799 return d_list | |
800 | |
801 d = self.dbpool.runQuery("SELECT profile_id,value FROM param_ind WHERE category=? AND name=?", xmpp_pass_path) | |
802 d.addCallback(encrypt_values) | |
803 return d |