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
9 changed files with 478 additions and 166 deletions
|
|
@ -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