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
8 changed files with 478 additions and 166 deletions
|
|
@ -194,16 +194,16 @@ class DbusObject(dbus.service.Object):
|
|||
return self._callback("addContact", unicode(entity_jid), unicode(profile_key))
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
|
||||
in_signature='s', out_signature='',
|
||||
in_signature='ss', out_signature='b',
|
||||
async_callbacks=('callback', 'errback'))
|
||||
def asyncConnect(self, profile_key="@DEFAULT@", callback=None, errback=None):
|
||||
return self._callback("asyncConnect", unicode(profile_key), callback=callback, errback=errback)
|
||||
def asyncConnect(self, profile_key="@DEFAULT@", password='', callback=None, errback=None):
|
||||
return self._callback("asyncConnect", unicode(profile_key), unicode(password), callback=callback, errback=errback)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
|
||||
in_signature='s', out_signature='',
|
||||
in_signature='ss', out_signature='',
|
||||
async_callbacks=('callback', 'errback'))
|
||||
def asyncCreateProfile(self, profile, callback=None, errback=None):
|
||||
return self._callback("asyncCreateProfile", unicode(profile), callback=callback, errback=errback)
|
||||
def asyncCreateProfile(self, profile, password='', callback=None, errback=None):
|
||||
return self._callback("asyncCreateProfile", unicode(profile), unicode(password), callback=callback, errback=errback)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
|
||||
in_signature='s', out_signature='',
|
||||
|
|
@ -224,10 +224,10 @@ class DbusObject(dbus.service.Object):
|
|||
return self._callback("confirmationAnswer", unicode(id), accepted, data, unicode(profile))
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
|
||||
in_signature='s', out_signature='',
|
||||
in_signature='ss', out_signature='b',
|
||||
async_callbacks=None)
|
||||
def connect(self, profile_key="@DEFAULT@"):
|
||||
return self._callback("connect", unicode(profile_key))
|
||||
def connect(self, profile_key="@DEFAULT@", password=''):
|
||||
return self._callback("connect", unicode(profile_key), unicode(password))
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
|
||||
in_signature='ss', out_signature='',
|
||||
|
|
|
|||
|
|
@ -204,10 +204,12 @@ doc_return=dictionary of asked key,
|
|||
async=
|
||||
type=method
|
||||
category=core
|
||||
sig_in=s
|
||||
sig_in=ss
|
||||
sig_out=
|
||||
param_1_default=''
|
||||
doc=Create a new profile
|
||||
doc_param_0=%(doc_profile)s
|
||||
doc_param_1=password: password of the profile
|
||||
doc_return=callback is called when profile actually exists in database and memory
|
||||
errback is called with error constant as parameter:
|
||||
- ConflictError: the profile name already exists
|
||||
|
|
@ -243,21 +245,35 @@ doc_param_4=port: port of the server to register to
|
|||
[connect]
|
||||
type=method
|
||||
category=core
|
||||
sig_in=s
|
||||
sig_out=
|
||||
sig_in=ss
|
||||
sig_out=b
|
||||
param_0_default="@DEFAULT@"
|
||||
param_1_default=''
|
||||
doc=Connect a profile
|
||||
doc_param_0=%(doc_profile_key)s
|
||||
doc_param_1=password: the SàT profile password
|
||||
doc_return=a deferred boolean or failure:
|
||||
- boolean if the profile authentication succeed:
|
||||
- True if the XMPP connection was already established
|
||||
- False if the XMPP connection has been initiated (it may still fail)
|
||||
- failure if the profile authentication failed
|
||||
|
||||
[asyncConnect]
|
||||
async=
|
||||
type=method
|
||||
category=core
|
||||
sig_in=s
|
||||
sig_out=
|
||||
sig_in=ss
|
||||
sig_out=b
|
||||
param_0_default="@DEFAULT@"
|
||||
param_1_default=''
|
||||
doc=Connect a profile
|
||||
doc_param_0=%(doc_profile_key)s
|
||||
doc_param_1=password: the SàT profile password
|
||||
doc_return=a deferred boolean or failure:
|
||||
- boolean if the profile authentication succeed:
|
||||
- True if the XMPP connection was already established
|
||||
- False if the XMPP connection has been initiated (it may still fail)
|
||||
- failure if the profile authentication failed
|
||||
|
||||
[disconnect]
|
||||
type=method
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ class Const(object):
|
|||
NO_SECURITY_LIMIT = -1
|
||||
INDIVIDUAL = "individual"
|
||||
GENERAL = "general"
|
||||
# Parameters related to encryption
|
||||
PROFILE_PASS_PATH = ('General', 'Password')
|
||||
MEMORY_CRYPTO_NAMESPACE = 'crypto' # for the private persistent binary dict
|
||||
MEMORY_CRYPTO_KEY = 'personal_key'
|
||||
# Parameters for static blog pages
|
||||
STATIC_BLOG_KEY = "Blog page"
|
||||
STATIC_BLOG_PARAM_TITLE = "Title"
|
||||
|
|
|
|||
|
|
@ -88,3 +88,7 @@ class BridgeExceptionNoService(Exception):
|
|||
|
||||
class DatabaseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PasswordError(Exception):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ from sat.core.log import getLogger
|
|||
log = getLogger(__name__)
|
||||
from sat.core.constants import Const as C
|
||||
from sat.memory.memory import Memory
|
||||
from sat.memory.crypto import PasswordHasher
|
||||
from sat.tools.misc import TriggerManager
|
||||
from sat.stdui import ui_contact_list
|
||||
from glob import glob
|
||||
|
|
@ -198,13 +199,25 @@ class SAT(service.Service):
|
|||
self.plugins[import_name].is_handler = False
|
||||
#TODO: test xmppclient presence and register handler parent
|
||||
|
||||
def connect(self, profile_key=C.PROF_KEY_NONE):
|
||||
"""Connect to jabber server"""
|
||||
self.asyncConnect(profile_key)
|
||||
def connect(self, profile_key=C.PROF_KEY_NONE, password=''):
|
||||
"""Connect to jabber server
|
||||
|
||||
def asyncConnect(self, profile_key=C.PROF_KEY_NONE):
|
||||
"""Connect to jabber server with asynchronous reply
|
||||
@param profile_key: %(doc_profile)s
|
||||
@param password (string): the SàT profile password
|
||||
@param profile_key: %(doc_profile_key)s
|
||||
"""
|
||||
self.asyncConnect(profile_key, password)
|
||||
|
||||
def asyncConnect(self, profile_key=C.PROF_KEY_NONE, password=''):
|
||||
"""Retrieve the individual parameters, authenticate the profile
|
||||
and initiate the connection to the associated XMPP server.
|
||||
|
||||
@param password (string): the SàT profile password
|
||||
@param profile_key: %(doc_profile_key)s
|
||||
@return: Deferred:
|
||||
- a deferred boolean if the profile authentication succeed:
|
||||
- True if the XMPP connection was already established
|
||||
- False if the XMPP connection has been initiated (it may still fail)
|
||||
- a Failure if the profile authentication failed
|
||||
"""
|
||||
def backendInitialised(dummy):
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
|
|
@ -212,85 +225,111 @@ class SAT(service.Service):
|
|||
log.error(_('Trying to connect a non-existant profile'))
|
||||
raise exceptions.ProfileUnknownError(profile_key)
|
||||
|
||||
if self.isConnected(profile):
|
||||
log.info(_("already connected !"))
|
||||
return defer.succeed("None")
|
||||
def connectXMPPClient(dummy=None):
|
||||
if self.isConnected(profile):
|
||||
log.info(_("already connected !"))
|
||||
return True
|
||||
self.memory.startProfileSession(profile)
|
||||
d = self.memory.loadIndividualParams(profile)
|
||||
d.addCallback(lambda dummy: self._connectXMPPClient(profile))
|
||||
return d.addCallback(lambda dummy: False)
|
||||
|
||||
def afterMemoryInit(ignore):
|
||||
"""This part must be called when we have loaded individual parameters from memory"""
|
||||
try:
|
||||
port = int(self.memory.getParamA("Port", "Connection", profile_key=profile))
|
||||
except ValueError:
|
||||
log.error(_("Can't parse port value, using default value"))
|
||||
port = 5222
|
||||
d = self._authenticateProfile(password, profile)
|
||||
d.addCallback(connectXMPPClient)
|
||||
return d
|
||||
|
||||
current = self.profiles[profile] = xmpp.SatXMPPClient(
|
||||
self, profile,
|
||||
jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key=profile), profile),
|
||||
self.memory.getParamA("Password", "Connection", profile_key=profile),
|
||||
self.memory.getParamA("Server", "Connection", profile_key=profile),
|
||||
port)
|
||||
|
||||
current.messageProt = xmpp.SatMessageProtocol(self)
|
||||
current.messageProt.setHandlerParent(current)
|
||||
|
||||
current.roster = xmpp.SatRosterProtocol(self)
|
||||
current.roster.setHandlerParent(current)
|
||||
|
||||
current.presence = xmpp.SatPresenceProtocol(self)
|
||||
current.presence.setHandlerParent(current)
|
||||
|
||||
current.fallBack = xmpp.SatFallbackHandler(self)
|
||||
current.fallBack.setHandlerParent(current)
|
||||
|
||||
current.versionHandler = xmpp.SatVersionHandler(C.APP_NAME_FULL,
|
||||
C.APP_VERSION)
|
||||
current.versionHandler.setHandlerParent(current)
|
||||
|
||||
current.identityHandler = xmpp.SatIdentityHandler()
|
||||
current.identityHandler.setHandlerParent(current)
|
||||
|
||||
log.debug(_("setting plugins parents"))
|
||||
|
||||
plugin_conn_cb = []
|
||||
for plugin in self.plugins.iteritems():
|
||||
if plugin[1].is_handler:
|
||||
plugin[1].getHandler(profile).setHandlerParent(current)
|
||||
connected_cb = getattr(plugin[1], "profileConnected", None)
|
||||
if connected_cb:
|
||||
plugin_conn_cb.append((plugin[0], connected_cb))
|
||||
|
||||
current.startService()
|
||||
|
||||
d = current.getConnectionDeferred()
|
||||
d.addCallback(lambda dummy: current.roster.got_roster) # we want to be sure that we got the roster
|
||||
|
||||
def pluginsConnection(dummy):
|
||||
"""Call profileConnected callback for all plugins, and print error message if any of them fails"""
|
||||
conn_cb_list = []
|
||||
for dummy, callback in plugin_conn_cb:
|
||||
conn_cb_list.append(defer.maybeDeferred(callback, profile))
|
||||
list_d = defer.DeferredList(conn_cb_list)
|
||||
|
||||
def logPluginResults(results):
|
||||
all_succeed = all([success for success, result in results])
|
||||
if not all_succeed:
|
||||
log.error(_("Plugins initialisation error"))
|
||||
for idx, (success, result) in enumerate(results):
|
||||
if not success:
|
||||
log.error("error (plugin %(name)s): %(failure)s" % {'name': plugin_conn_cb[idx][0],
|
||||
'failure': result})
|
||||
|
||||
list_d.addCallback(logPluginResults)
|
||||
return list_d
|
||||
|
||||
d.addCallback(pluginsConnection)
|
||||
return d
|
||||
|
||||
self.memory.startProfileSession(profile)
|
||||
return self.memory.loadIndividualParams(profile).addCallback(afterMemoryInit)
|
||||
return self._initialised.addCallback(backendInitialised)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _connectXMPPClient(self, profile):
|
||||
"""This part is called from asyncConnect when we have loaded individual parameters from memory"""
|
||||
try:
|
||||
port = int(self.memory.getParamA("Port", "Connection", profile_key=profile))
|
||||
except ValueError:
|
||||
log.error(_("Can't parse port value, using default value"))
|
||||
port = 5222
|
||||
|
||||
password = yield self.memory.asyncGetParamA("Password", "Connection", profile_key=profile)
|
||||
current = self.profiles[profile] = xmpp.SatXMPPClient(self, profile,
|
||||
jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key=profile)),
|
||||
password, self.memory.getParamA("Server", "Connection", profile_key=profile), port)
|
||||
|
||||
current.messageProt = xmpp.SatMessageProtocol(self)
|
||||
current.messageProt.setHandlerParent(current)
|
||||
|
||||
current.roster = xmpp.SatRosterProtocol(self)
|
||||
current.roster.setHandlerParent(current)
|
||||
|
||||
current.presence = xmpp.SatPresenceProtocol(self)
|
||||
current.presence.setHandlerParent(current)
|
||||
|
||||
current.fallBack = xmpp.SatFallbackHandler(self)
|
||||
current.fallBack.setHandlerParent(current)
|
||||
|
||||
current.versionHandler = xmpp.SatVersionHandler(C.APP_NAME_FULL,
|
||||
C.APP_VERSION)
|
||||
current.versionHandler.setHandlerParent(current)
|
||||
|
||||
current.identityHandler = xmpp.SatIdentityHandler()
|
||||
current.identityHandler.setHandlerParent(current)
|
||||
|
||||
log.debug(_("setting plugins parents"))
|
||||
|
||||
plugin_conn_cb = []
|
||||
for plugin in self.plugins.iteritems():
|
||||
if plugin[1].is_handler:
|
||||
plugin[1].getHandler(profile).setHandlerParent(current)
|
||||
connected_cb = getattr(plugin[1], "profileConnected", None)
|
||||
if connected_cb:
|
||||
plugin_conn_cb.append((plugin[0], connected_cb))
|
||||
|
||||
current.startService()
|
||||
|
||||
yield current.getConnectionDeferred()
|
||||
yield current.roster.got_roster # we want to be sure that we got the roster
|
||||
|
||||
# Call profileConnected callback for all plugins, and print error message if any of them fails
|
||||
conn_cb_list = []
|
||||
for dummy, callback in plugin_conn_cb:
|
||||
conn_cb_list.append(defer.maybeDeferred(callback, profile))
|
||||
list_d = defer.DeferredList(conn_cb_list)
|
||||
|
||||
def logPluginResults(results):
|
||||
all_succeed = all([success for success, result in results])
|
||||
if not all_succeed:
|
||||
log.error(_("Plugins initialisation error"))
|
||||
for idx, (success, result) in enumerate(results):
|
||||
if not success:
|
||||
log.error("error (plugin %(name)s): %(failure)s" %
|
||||
{'name': plugin_conn_cb[idx][0], 'failure': result})
|
||||
|
||||
yield list_d.addCallback(logPluginResults)
|
||||
|
||||
def _authenticateProfile(self, password, profile):
|
||||
"""Authenticate the profile.
|
||||
|
||||
@param password (string): the SàT profile password
|
||||
@param profile_key: %(doc_profile_key)s
|
||||
@return: Deferred: a deferred None in case of success, a failure otherwise.
|
||||
"""
|
||||
session_data = self.memory.auth_sessions.profileGetUnique(profile)
|
||||
if not password and session_data:
|
||||
# XXX: this allows any frontend to connect with the empty password as soon as
|
||||
# the profile has been authenticated at least once before. It is OK as long as
|
||||
# submitting a form with empty passwords is restricted to local frontends.
|
||||
return defer.succeed(None)
|
||||
|
||||
def check_result(result):
|
||||
if not result:
|
||||
log.warning(_('Authentication failure of profile %s') % profile)
|
||||
raise exceptions.PasswordError(_("The provided profile password doesn't match."))
|
||||
if not session_data: # avoid to create two profile sessions when password if specified
|
||||
return self.memory.newAuthSession(password, profile)
|
||||
|
||||
d = self.memory.asyncGetParamA(C.PROFILE_PASS_PATH[1], C.PROFILE_PASS_PATH[0], profile_key=profile)
|
||||
d.addCallback(lambda sat_cipher: PasswordHasher.verify(password, sat_cipher))
|
||||
return d.addCallback(check_result)
|
||||
|
||||
def disconnect(self, profile_key):
|
||||
"""disconnect from jabber server"""
|
||||
if not self.isConnected(profile_key):
|
||||
|
|
@ -388,21 +427,24 @@ class SAT(service.Service):
|
|||
def registerNewAccountCB(self, data, profile):
|
||||
# FIXME: to be removed/redone elsewhere
|
||||
user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0]
|
||||
password = self.memory.getParamA("Password", "Connection", profile_key=profile)
|
||||
server = self.memory.getParamA("Server", "Connection", profile_key=profile)
|
||||
d = self.memory.asyncGetParamA("Password", "Connection", profile_key=profile)
|
||||
|
||||
if not user or not password or not server:
|
||||
raise exceptions.DataError(_("No user, password or server given, can't register new account."))
|
||||
def gotPassword(password):
|
||||
if not user or not password or not server:
|
||||
raise exceptions.DataError(_("No user, password or server given, can't register new account."))
|
||||
|
||||
# FIXME: to be fixed with XMLUI dialogs once their implemented
|
||||
confirm_id = sat_next_id()
|
||||
self.__private_data[confirm_id] = (id, profile)
|
||||
# FIXME: to be fixed with XMLUI dialogs once their implemented
|
||||
confirm_id = sat_next_id()
|
||||
self.__private_data[confirm_id] = (id, profile)
|
||||
|
||||
self.askConfirmation(
|
||||
confirm_id, "YES/NO",
|
||||
{"message": _("Are you sure to register new account [%(user)s] to server %(server)s ?") % {'user': user, 'server': server, 'profile': profile}},
|
||||
self.regisConfirmCB, profile)
|
||||
print "===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============"
|
||||
self.askConfirmation(
|
||||
confirm_id, "YES/NO",
|
||||
{"message": _("Are you sure to register new account [%(user)s] to server %(server)s ?") % {'user': user, 'server': server, 'profile': profile}},
|
||||
self.regisConfirmCB, profile)
|
||||
print "===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============"
|
||||
|
||||
d.addCallback(gotPassword)
|
||||
|
||||
def regisConfirmCB(self, id, accepted, data, profile):
|
||||
print _("register Confirmation CB ! (%s)") % str(accepted)
|
||||
|
|
@ -410,9 +452,9 @@ class SAT(service.Service):
|
|||
del self.__private_data[id]
|
||||
if accepted:
|
||||
user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0]
|
||||
password = self.memory.getParamA("Password", "Connection", profile_key=profile)
|
||||
server = self.memory.getParamA("Server", "Connection", profile_key=profile)
|
||||
self.registerNewAccount(user, password, None, server, id=action_id)
|
||||
d = self.memory.asyncGetParamA("Password", "Connection", profile_key=profile)
|
||||
d.addCallback(lambda password: self.registerNewAccount(user, password, None, server, id=action_id))
|
||||
else:
|
||||
self.actionResult(action_id, "SUPPRESS", {}, profile)
|
||||
|
||||
|
|
@ -420,18 +462,17 @@ class SAT(service.Service):
|
|||
|
||||
def setParam(self, name, value, category, security_limit, profile_key):
|
||||
"""set wanted paramater and notice observers"""
|
||||
log.info(_("setting param: %(name)s=%(value)s in category %(category)s") % {'name': name, 'value': value, 'category': category})
|
||||
self.memory.setParam(name, value, category, security_limit, profile_key)
|
||||
|
||||
def isConnected(self, profile_key):
|
||||
"""Return connection status of profile
|
||||
@param profile_key: key_word or profile name to determine profile name
|
||||
@return True if connected
|
||||
@return: True if connected
|
||||
"""
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
if not profile:
|
||||
log.error(_('asking connection status for a non-existant profile'))
|
||||
return
|
||||
raise exceptions.ProfileUnknownError(profile_key)
|
||||
if profile not in self.profiles:
|
||||
return False
|
||||
return self.profiles[profile].isConnected()
|
||||
|
|
|
|||
|
|
@ -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