changeset 1030:15f43b54d697

core, memory, bridge: added profile password + password encryption: /!\ This changeset updates the database version to 2 and modify the database content! Description: - new parameter General / Password to store the profile password - profile password is initialized with XMPP password value, it is stored hashed - bridge methods asyncCreateProfile/asyncConnect takes a new argument "password" (default = "") - bridge method asyncConnect returns a boolean (True = connection already established, False = connection initiated) - profile password is checked before initializing the XMPP connection - new private individual parameter to store the personal encryption key of each profile - personal key is randomly generated and encrypted with the profile password - personal key is decrypted after profile authentification and stored in a Sessions instance - personal key is used to encrypt/decrypt other passwords when they need to be retrieved/modified - modifying the profile password re-encrypt the personal key - Memory.setParam now returns a Deferred (the bridge method "setParam" is unchanged) - Memory.asyncGetParamA eventually decrypts the password, Memory.getParamA would fail on a password parameter TODO: - if profile authentication is OK but XMPP authentication is KO, prompt the user for another XMPP password - fix the method "registerNewAccount" (and move it to a plugin) - remove bridge method "connect", sole "asyncConnect" should be used
author souliane <souliane@mailoo.org>
date Wed, 07 May 2014 16:02:23 +0200
parents f6182f6418ea
children e90125d07072
files frontends/src/bridge/DBus.py src/bridge/DBus.py src/bridge/bridge_constructor/bridge_template.ini src/core/constants.py src/core/exceptions.py src/core/sat_main.py src/memory/memory.py src/memory/params.py src/memory/sqlite.py
diffstat 9 files changed, 465 insertions(+), 153 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/bridge/DBus.py	Sat May 10 17:37:32 2014 +0200
+++ b/frontends/src/bridge/DBus.py	Wed May 07 16:02:23 2014 +0200
@@ -99,11 +99,11 @@
     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 @@
     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)
--- a/src/bridge/DBus.py	Sat May 10 17:37:32 2014 +0200
+++ b/src/bridge/DBus.py	Wed May 07 16:02:23 2014 +0200
@@ -194,16 +194,16 @@
         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 @@
         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='',
--- a/src/bridge/bridge_constructor/bridge_template.ini	Sat May 10 17:37:32 2014 +0200
+++ b/src/bridge/bridge_constructor/bridge_template.ini	Wed May 07 16:02:23 2014 +0200
@@ -204,10 +204,12 @@
 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 @@
 [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
--- a/src/core/constants.py	Sat May 10 17:37:32 2014 +0200
+++ b/src/core/constants.py	Wed May 07 16:02:23 2014 +0200
@@ -40,6 +40,10 @@
     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"
--- a/src/core/exceptions.py	Sat May 10 17:37:32 2014 +0200
+++ b/src/core/exceptions.py	Wed May 07 16:02:23 2014 +0200
@@ -88,3 +88,7 @@
 
 class DatabaseError(Exception):
     pass
+
+
+class PasswordError(Exception):
+    pass
--- a/src/core/sat_main.py	Sat May 10 17:37:32 2014 +0200
+++ b/src/core/sat_main.py	Wed May 07 16:02:23 2014 +0200
@@ -31,6 +31,7 @@
 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 @@
             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
+
+        @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):
-        """Connect to jabber server with asynchronous reply
-        @param profile_key: %(doc_profile)s
+    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,84 +225,110 @@
                 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
+
+        return self._initialised.addCallback(backendInitialised)
 
-                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)
+    @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
 
-                current.messageProt = xmpp.SatMessageProtocol(self)
-                current.messageProt.setHandlerParent(current)
+        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.roster = xmpp.SatRosterProtocol(self)
-                current.roster.setHandlerParent(current)
+        current.messageProt = xmpp.SatMessageProtocol(self)
+        current.messageProt.setHandlerParent(current)
 
-                current.presence = xmpp.SatPresenceProtocol(self)
-                current.presence.setHandlerParent(current)
+        current.roster = xmpp.SatRosterProtocol(self)
+        current.roster.setHandlerParent(current)
 
-                current.fallBack = xmpp.SatFallbackHandler(self)
-                current.fallBack.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.versionHandler = xmpp.SatVersionHandler(C.APP_NAME_FULL,
+                                                        C.APP_VERSION)
+        current.versionHandler.setHandlerParent(current)
 
-                current.identityHandler = xmpp.SatIdentityHandler()
-                current.identityHandler.setHandlerParent(current)
+        current.identityHandler = xmpp.SatIdentityHandler()
+        current.identityHandler.setHandlerParent(current)
+
+        log.debug(_("setting plugins parents"))
 
-                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))
 
-                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
 
-                current.startService()
-
-                d = current.getConnectionDeferred()
-                d.addCallback(lambda dummy: 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 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})
+
+        yield list_d.addCallback(logPluginResults)
+
+    def _authenticateProfile(self, password, profile):
+        """Authenticate the profile.
 
-                    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})
+        @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)
 
-                    list_d.addCallback(logPluginResults)
-                    return list_d
+        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.addCallback(pluginsConnection)
-                return d
-
-            self.memory.startProfileSession(profile)
-            return self.memory.loadIndividualParams(profile).addCallback(afterMemoryInit)
-        return self._initialised.addCallback(backendInitialised)
+        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"""
@@ -388,21 +427,24 @@
     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 @@
         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 @@
 
     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()
--- a/src/memory/memory.py	Sat May 10 17:37:32 2014 +0200
+++ b/src/memory/memory.py	Wed May 07 16:02:23 2014 +0200
@@ -34,6 +34,7 @@
 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 @@
         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 @@
         """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 @@
         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 @@
         """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 @@
     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 @@
         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 @@
         """
         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 @@
             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)
--- a/src/memory/params.py	Sat May 10 17:37:32 2014 +0200
+++ b/src/memory/params.py	Wed May 07 16:02:23 2014 +0200
@@ -21,6 +21,7 @@
 
 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 @@
     <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 @@
     </individual>
     </params>
     """ % {
+        'category_general': _("General"),
         'category_connection': _("Connection"),
         'label_NewAccount': _("Register new account"),
         'label_autoconnect': _('Connect on frontend startup'),
@@ -311,11 +316,16 @@
                 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 @@
             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 @@
         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.
+
+        /!\ This method would return encrypted password values,
+            to get the plain values you have to use _asyncGetParamA.
 
-           @return: 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: 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 @@
         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 @@
         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 @@
                 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.
--- a/src/memory/sqlite.py	Sat May 10 17:37:32 2014 +0200
+++ b/src/memory/sqlite.py	Wed May 07 16:02:23 2014 +0200
@@ -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 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 @@
 #      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 @@
                                                    ("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 @@
                               ('message_types', (("'chat'",), ("'error'",), ("'groupchat'",), ("'headline'",), ("'normal'",))),
                               )),
                     },
+        2:         {'specific': 'update2raw_v2'
+                   },
         1:         {'cols create': {'history': ('extra BLOB',)}
                    },
         }
@@ -508,7 +511,7 @@
 
     @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 @@
         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 @@
                     # 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 @@
                 '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 @@
         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