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:
souliane 2014-05-07 16:02:23 +02:00
commit 0d28c03d27
9 changed files with 478 additions and 166 deletions

View file

@ -99,11 +99,11 @@ class DBusBridgeFrontend(BridgeFrontend):
def addContact(self, entity_jid, profile_key="@DEFAULT@"):
return self.db_core_iface.addContact(entity_jid, profile_key)
def asyncConnect(self, profile_key="@DEFAULT@", callback=None, errback=None):
return self.db_core_iface.asyncConnect(profile_key, reply_handler=callback, error_handler=lambda err:errback(err._dbus_error_name[len(const_ERROR_PREFIX)+1:]))
def asyncConnect(self, profile_key="@DEFAULT@", password='', callback=None, errback=None):
return self.db_core_iface.asyncConnect(profile_key, password, reply_handler=callback, error_handler=lambda err:errback(err._dbus_error_name[len(const_ERROR_PREFIX)+1:]))
def asyncCreateProfile(self, profile, callback=None, errback=None):
return self.db_core_iface.asyncCreateProfile(profile, reply_handler=callback, error_handler=lambda err:errback(err._dbus_error_name[len(const_ERROR_PREFIX)+1:]))
def asyncCreateProfile(self, profile, password='', callback=None, errback=None):
return self.db_core_iface.asyncCreateProfile(profile, password, reply_handler=callback, error_handler=lambda err:errback(err._dbus_error_name[len(const_ERROR_PREFIX)+1:]))
def asyncDeleteProfile(self, profile, callback=None, errback=None):
return self.db_core_iface.asyncDeleteProfile(profile, reply_handler=callback, error_handler=lambda err:errback(err._dbus_error_name[len(const_ERROR_PREFIX)+1:]))
@ -114,8 +114,8 @@ class DBusBridgeFrontend(BridgeFrontend):
def confirmationAnswer(self, id, accepted, data, profile):
return self.db_core_iface.confirmationAnswer(id, accepted, data, profile)
def connect(self, profile_key="@DEFAULT@"):
return self.db_core_iface.connect(profile_key)
def connect(self, profile_key="@DEFAULT@", password=''):
return self.db_core_iface.connect(profile_key, password)
def delContact(self, entity_jid, profile_key="@DEFAULT@"):
return self.db_core_iface.delContact(entity_jid, profile_key)

View file

@ -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='',

View file

@ -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

View file

@ -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"

View file

@ -88,3 +88,7 @@ class BridgeExceptionNoService(Exception):
class DatabaseError(Exception):
pass
class PasswordError(Exception):
pass

View file

@ -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()

View file

@ -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)

View file

@ -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.

View file

@ -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