comparison 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
comparison
equal deleted inserted replaced
1029:f6182f6418ea 1030:15f43b54d697
32 from sat.core.constants import Const as C 32 from sat.core.constants import Const as C
33 from sat.memory.sqlite import SqliteStorage 33 from sat.memory.sqlite import SqliteStorage
34 from sat.memory.persistent import PersistentDict 34 from sat.memory.persistent import PersistentDict
35 from sat.memory.params import Params 35 from sat.memory.params import Params
36 from sat.memory.disco import Discovery 36 from sat.memory.disco import Discovery
37 from sat.memory.crypto import BlockCipher
37 38
38 39
39 class Sessions(object): 40 class Sessions(object):
40 """Sessions are data associated to key used for a temporary moment, with optional profile checking.""" 41 """Sessions are data associated to key used for a temporary moment, with optional profile checking."""
41 DEFAULT_TIMEOUT = 600 42 DEFAULT_TIMEOUT = 600
208 self.initialized = defer.Deferred() 209 self.initialized = defer.Deferred()
209 self.host = host 210 self.host = host
210 self._entities_cache = {} # XXX: keep presence/last resource/other data in cache 211 self._entities_cache = {} # XXX: keep presence/last resource/other data in cache
211 # /!\ an entity is not necessarily in roster 212 # /!\ an entity is not necessarily in roster
212 self.subscriptions = {} 213 self.subscriptions = {}
214 self.auth_sessions = ProfileSessions() # remember the authenticated profiles
213 self.disco = Discovery(host) 215 self.disco = Discovery(host)
214 fixLocalDir(False) # XXX: tmp update code, will be removed in the future 216 fixLocalDir(False) # XXX: tmp update code, will be removed in the future
215 self.config = self.parseMainConf() 217 self.config = self.parseMainConf()
216 database_file = os.path.expanduser(os.path.join(self.getConfig('', 'local_dir'), C.SAVEFILE_DATABASE)) 218 database_file = os.path.expanduser(os.path.join(self.getConfig('', 'local_dir'), C.SAVEFILE_DATABASE))
217 self.storage = SqliteStorage(database_file, host.__version__) 219 self.storage = SqliteStorage(database_file, host.__version__)
257 259
258 def load_xml(self, filename): 260 def load_xml(self, filename):
259 """Load parameters template from xml file 261 """Load parameters template from xml file
260 262
261 @param filename (str): input file 263 @param filename (str): input file
262 @return bool: True in case of success 264 @return: bool: True in case of success
263 """ 265 """
264 if not filename: 266 if not filename:
265 return False 267 return False
266 filename = os.path.expanduser(filename) 268 filename = os.path.expanduser(filename)
267 if os.path.exists(filename): 269 if os.path.exists(filename):
287 """"Iniatialise session for a profile 289 """"Iniatialise session for a profile
288 @param profile: %(doc_profile)s""" 290 @param profile: %(doc_profile)s"""
289 log.info(_("[%s] Profile session started" % profile)) 291 log.info(_("[%s] Profile session started" % profile))
290 self._entities_cache[profile] = {} 292 self._entities_cache[profile] = {}
291 293
294 def newAuthSession(self, key, profile):
295 """Start a new session for the authenticated profile.
296
297 The personal key is loaded encrypted from a PersistentDict before being decrypted.
298
299 @param key: the key to decrypt the personal key
300 @param profile: %(doc_profile)s
301 @return: a deferred None value
302 """
303 def gotPersonalKey(personal_key):
304 """Create the session for this profile and store the personal key"""
305 self.auth_sessions.newSession({C.MEMORY_CRYPTO_KEY: personal_key}, profile)
306 log.debug('auth session created for profile %s' % profile)
307
308 d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
309 d.addCallback(lambda data: BlockCipher.decrypt(key, data[C.MEMORY_CRYPTO_KEY]))
310 return d.addCallback(gotPersonalKey)
311
292 def purgeProfileSession(self, profile): 312 def purgeProfileSession(self, profile):
293 """Delete cache of data of profile 313 """Delete cache of data of profile
294 @param profile: %(doc_profile)s""" 314 @param profile: %(doc_profile)s"""
295 log.info(_("[%s] Profile session purge" % profile)) 315 log.info(_("[%s] Profile session purge" % profile))
296 self.params.purgeProfile(profile) 316 self.params.purgeProfile(profile)
301 321
302 def save_xml(self, filename): 322 def save_xml(self, filename):
303 """Save parameters template to xml file 323 """Save parameters template to xml file
304 324
305 @param filename (str): output file 325 @param filename (str): output file
306 @return bool: True in case of success 326 @return: bool: True in case of success
307 """ 327 """
308 if not filename: 328 if not filename:
309 return False 329 return False
310 #TODO: need to encrypt files (at least passwords !) and set permissions 330 #TODO: need to encrypt files (at least passwords !) and set permissions
311 filename = os.path.expanduser(filename) 331 filename = os.path.expanduser(filename)
318 return False 338 return False
319 339
320 def getProfilesList(self): 340 def getProfilesList(self):
321 return self.storage.getProfilesList() 341 return self.storage.getProfilesList()
322 342
323 def getProfileName(self, profile_key, return_profile_keys = False): 343 def getProfileName(self, profile_key, return_profile_keys=False):
324 """Return name of profile from keyword 344 """Return name of profile from keyword
325 @param profile_key: can be the profile name or a keywork (like @DEFAULT@) 345 @param profile_key: can be the profile name or a keywork (like @DEFAULT@)
326 @return: profile name or None if it doesn't exist""" 346 @return: profile name or None if it doesn't exist"""
327 return self.params.getProfileName(profile_key, return_profile_keys) 347 return self.params.getProfileName(profile_key, return_profile_keys)
328 348
329 def asyncCreateProfile(self, name): 349 def asyncCreateProfile(self, name, password=''):
330 """Create a new profile 350 """Create a new profile
331 @param name: Profile name 351 @param name: profile name
332 """ 352 @param password: profile password
333 return self.params.asyncCreateProfile(name) 353 @return: Deferred
354 """
355 personal_key = BlockCipher.getRandomKey(base64=True) # generated once for all and saved in a PersistentDict
356 self.auth_sessions.newSession({C.MEMORY_CRYPTO_KEY: personal_key}, name) # will be encrypted by setParam
357 d = self.params.asyncCreateProfile(name)
358 d.addCallback(lambda dummy: self.setParam(C.PROFILE_PASS_PATH[1], password, C.PROFILE_PASS_PATH[0], profile_key=name))
359 return d
334 360
335 def asyncDeleteProfile(self, name, force=False): 361 def asyncDeleteProfile(self, name, force=False):
336 """Delete an existing profile 362 """Delete an existing profile
337 @param name: Name of the profile 363 @param name: Name of the profile
338 @param force: force the deletion even if the profile is connected. 364 @param force: force the deletion even if the profile is connected.
352 return self.storage.getHistory(jid.JID(from_jid), jid.JID(to_jid), limit, between, profile) 378 return self.storage.getHistory(jid.JID(from_jid), jid.JID(to_jid), limit, between, profile)
353 379
354 def _getLastResource(self, jid_s, profile_key): 380 def _getLastResource(self, jid_s, profile_key):
355 jid_ = jid.JID(jid_s) 381 jid_ = jid.JID(jid_s)
356 return self.getLastResource(jid_, profile_key) or "" 382 return self.getLastResource(jid_, profile_key) or ""
357
358 383
359 def getLastResource(self, entity_jid, profile_key): 384 def getLastResource(self, entity_jid, profile_key):
360 """Return the last resource used by an entity 385 """Return the last resource used by an entity
361 @param entity_jid: entity jid 386 @param entity_jid: entity jid
362 @param profile_key: %(doc_profile_key)s""" 387 @param profile_key: %(doc_profile_key)s"""
500 @raise: exceptions.UnknownEntityError if entity is not in cache 525 @raise: exceptions.UnknownEntityError if entity is not in cache
501 @raise: KeyError if there is no value for this key and this entity 526 @raise: KeyError if there is no value for this key and this entity
502 """ 527 """
503 return self.getEntityData(entity_jid, (key,), profile_key)[key] 528 return self.getEntityData(entity_jid, (key,), profile_key)[key]
504 529
505
506 def delEntityCache(self, entity_jid, delete_all_resources=True, profile_key=C.PROF_KEY_NONE): 530 def delEntityCache(self, entity_jid, delete_all_resources=True, profile_key=C.PROF_KEY_NONE):
507 """Remove cached data for entity 531 """Remove cached data for entity
508 @param entity_jid: JID of the entity to delete 532 @param entity_jid: JID of the entity to delete
509 @param delete_all_resources: if True also delete all known resources form cache 533 @param delete_all_resources: if True also delete all known resources form cache
510 @param profile_key: %(doc_profile_key)s 534 @param profile_key: %(doc_profile_key)s
525 try: 549 try:
526 del self._entities_cache[profile][entity] 550 del self._entities_cache[profile][entity]
527 except KeyError: 551 except KeyError:
528 log.debug("Can't delete entity [%s]: not in cache" % entity.full()) 552 log.debug("Can't delete entity [%s]: not in cache" % entity.full())
529 553
554 def encryptPersonalData(self, data_key, data_value, crypto_key, profile):
555 """Re-encrypt a personal data (saved to a PersistentDict).
556
557 @param data_key: key for the individual PersistentDict instance
558 @param data_value: the value to be encrypted
559 @param crypto_key: the key to encrypt the value
560 @param profile: %(profile_doc)s
561 @return: a deferred None value
562 """
563
564 def gotIndMemory(data):
565 d = BlockCipher.encrypt(crypto_key, data_value)
566
567 def cb(cipher):
568 data[data_key] = cipher
569 return data.force(data_key)
570
571 return d.addCallback(cb)
572
573 def done(dummy):
574 log.debug(_('Personal data (%(ns)s, %(key)s) has been successfuly encrypted') %
575 {'ns': C.MEMORY_CRYPTO_NAMESPACE, 'key': data_key})
576
577 d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
578 return d.addCallback(gotIndMemory).addCallback(done)
579
530 def addWaitingSub(self, type_, entity_jid, profile_key): 580 def addWaitingSub(self, type_, entity_jid, profile_key):
531 """Called when a subcription request is received""" 581 """Called when a subcription request is received"""
532 profile = self.getProfileName(profile_key) 582 profile = self.getProfileName(profile_key)
533 assert profile 583 assert profile
534 if profile not in self.subscriptions: 584 if profile not in self.subscriptions: