Mercurial > libervia-backend
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: |