diff --git a/frontends/src/bridge/DBus.py b/frontends/src/bridge/DBus.py
index 10fde6aa..0c695173 100644
--- a/frontends/src/bridge/DBus.py
+++ b/frontends/src/bridge/DBus.py
@@ -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)
diff --git a/src/bridge/DBus.py b/src/bridge/DBus.py
index 743149ad..15ccbab2 100755
--- a/src/bridge/DBus.py
+++ b/src/bridge/DBus.py
@@ -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='',
diff --git a/src/bridge/bridge_constructor/bridge_template.ini b/src/bridge/bridge_constructor/bridge_template.ini
index 3f9ec028..83bd985f 100644
--- a/src/bridge/bridge_constructor/bridge_template.ini
+++ b/src/bridge/bridge_constructor/bridge_template.ini
@@ -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
diff --git a/src/core/constants.py b/src/core/constants.py
index c2ae5c5c..f768b121 100644
--- a/src/core/constants.py
+++ b/src/core/constants.py
@@ -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"
diff --git a/src/core/exceptions.py b/src/core/exceptions.py
index ba1d57ca..c52c73d2 100644
--- a/src/core/exceptions.py
+++ b/src/core/exceptions.py
@@ -88,3 +88,7 @@ class BridgeExceptionNoService(Exception):
class DatabaseError(Exception):
pass
+
+
+class PasswordError(Exception):
+ pass
diff --git a/src/core/sat_main.py b/src/core/sat_main.py
index e9a3ad76..19e5c1f3 100644
--- a/src/core/sat_main.py
+++ b/src/core/sat_main.py
@@ -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()
diff --git a/src/memory/memory.py b/src/memory/memory.py
index c789ddf3..67efc19f 100644
--- a/src/memory/memory.py
+++ b/src/memory/memory.py
@@ -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)
diff --git a/src/memory/params.py b/src/memory/params.py
index 897a518b..133911a8 100644
--- a/src/memory/params.py
+++ b/src/memory/params.py
@@ -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):
+
+
+
@@ -57,6 +61,7 @@ class Params(object):
""" % {
+ '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.
diff --git a/src/memory/sqlite.py b/src/memory/sqlite.py
index c00d47cc..1d6aaf4d 100644
--- a/src/memory/sqlite.py
+++ b/src/memory/sqlite.py
@@ -18,9 +18,11 @@
# along with this program. If not, see .
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