diff src/memory/memory.py @ 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 65fffdcb97f1
line wrap: on
line diff
--- 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)