comparison sat/memory/memory.py @ 3160:330a5f1d9eea

core (memory/crypto): replaced `PyCrypto` by `cryptography`: `PyCrypto` is unmaintained for years but was used in SàT for password hashing. This patch fixes that by replacing `PyCrypto` by the reference `cryptography` module which is well maintained. The behaviour stays the same (except that previously async `hash`, `encrypt` and `decrypt` methods are now synchronous, as they are quick and using a deferToThread may actually be more resource intensive than using blocking methods). It is planed to improve `memory.crypto` by using more up-to-date cryptography/hashing algorithms in the future. PyCrypto is no more a dependency of SàT
author Goffi <goffi@goffi.org>
date Sun, 09 Feb 2020 23:50:26 +0100
parents 60a9e47ef988
children d10b2368684e
comparison
equal deleted inserted replaced
3159:30e08d904208 3160:330a5f1d9eea
345 ) 345 )
346 session_d.addCallback(createSession) 346 session_d.addCallback(createSession)
347 finally: 347 finally:
348 return session_d 348 return session_d
349 349
350 auth_d = self.profileAuthenticate(password, profile) 350 auth_d = defer.ensureDeferred(self.profileAuthenticate(password, profile))
351 auth_d.addCallback(doStartSession) 351 auth_d.addCallback(doStartSession)
352 return auth_d 352 return auth_d
353 353
354 if self.host.initialised.called: 354 if self.host.initialised.called:
355 return defer.succeed(None).addCallback(backendInitialised) 355 return defer.succeed(None).addCallback(backendInitialised)
379 # the session is starting but not started yet 379 # the session is starting but not started yet
380 return not isinstance(self._entities_cache[profile], defer.Deferred) 380 return not isinstance(self._entities_cache[profile], defer.Deferred)
381 except KeyError: 381 except KeyError:
382 return False 382 return False
383 383
384 def profileAuthenticate(self, password, profile): 384 async def profileAuthenticate(self, password, profile):
385 """Authenticate the profile. 385 """Authenticate the profile.
386 386
387 @param password (unicode): the SàT profile password 387 @param password (unicode): the SàT profile password
388 @param profile: %(doc_profile)s 388 @return: None in case of success (an exception is raised otherwise)
389 @return (D): a deferred None in case of success, a failure otherwise.
390 @raise exceptions.PasswordError: the password does not match 389 @raise exceptions.PasswordError: the password does not match
391 """ 390 """
392 if not password and self.auth_sessions.profileGetUnique(profile): 391 if not password and self.auth_sessions.profileGetUnique(profile):
393 # XXX: this allows any frontend to connect with the empty password as soon as 392 # XXX: this allows any frontend to connect with the empty password as soon as
394 # the profile has been authenticated at least once before. It is OK as long as 393 # the profile has been authenticated at least once before. It is OK as long as
395 # submitting a form with empty passwords is restricted to local frontends. 394 # submitting a form with empty passwords is restricted to local frontends.
396 return defer.succeed(None) 395 return
397 396
398 def check_result(result): 397 sat_cipher = await self.asyncGetParamA(
399 if not result:
400 log.warning("Authentication failure of profile {}".format(profile))
401 raise failure.Failure(
402 exceptions.PasswordError(
403 "The provided profile password doesn't match."
404 )
405 )
406 return self.newAuthSession(password, profile)
407
408 d = self.asyncGetParamA(
409 C.PROFILE_PASS_PATH[1], C.PROFILE_PASS_PATH[0], profile_key=profile 398 C.PROFILE_PASS_PATH[1], C.PROFILE_PASS_PATH[0], profile_key=profile
410 ) 399 )
411 d.addCallback(lambda sat_cipher: PasswordHasher.verify(password, sat_cipher)) 400 valid = PasswordHasher.verify(password, sat_cipher)
412 return d.addCallback(check_result) 401 if not valid:
413 402 log.warning(_("Authentication failure of profile {profile}").format(
414 def newAuthSession(self, key, profile): 403 profile=profile))
404 raise exceptions.PasswordError("The provided profile password doesn't match.")
405 return await self.newAuthSession(password, profile)
406
407 async def newAuthSession(self, key, profile):
415 """Start a new session for the authenticated profile. 408 """Start a new session for the authenticated profile.
416 409
417 If there is already an existing session, no new one is created 410 If there is already an existing session, no new one is created
418 The personal key is loaded encrypted from a PersistentDict before being decrypted. 411 The personal key is loaded encrypted from a PersistentDict before being decrypted.
419 412
420 @param key: the key to decrypt the personal key 413 @param key: the key to decrypt the personal key
421 @param profile: %(doc_profile)s 414 @param profile: %(doc_profile)s
422 @return: a deferred None value 415 """
423 """ 416 data = await PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
424 417 personal_key = BlockCipher.decrypt(key, data[C.MEMORY_CRYPTO_KEY])
425 def gotPersonalKey(personal_key): 418 # Create the session for this profile and store the personal key
426 """Create the session for this profile and store the personal key""" 419 session_data = self.auth_sessions.profileGetUnique(profile)
427 session_data = self.auth_sessions.profileGetUnique(profile) 420 if not session_data:
428 if not session_data: 421 self.auth_sessions.newSession(
429 self.auth_sessions.newSession( 422 {C.MEMORY_CRYPTO_KEY: personal_key}, profile=profile
430 {C.MEMORY_CRYPTO_KEY: personal_key}, profile=profile 423 )
431 ) 424 log.debug("auth session created for profile %s" % profile)
432 log.debug("auth session created for profile %s" % profile)
433
434 d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
435 d.addCallback(lambda data: BlockCipher.decrypt(key, data[C.MEMORY_CRYPTO_KEY]))
436 return d.addCallback(gotPersonalKey)
437 425
438 def purgeProfileSession(self, profile): 426 def purgeProfileSession(self, profile):
439 """Delete cache of data of profile 427 """Delete cache of data of profile
440 @param profile: %(doc_profile)s""" 428 @param profile: %(doc_profile)s"""
441 log.info(_("[%s] Profile session purge" % profile)) 429 log.info(_("[%s] Profile session purge" % profile))
1062 @param profile: %(profile_doc)s 1050 @param profile: %(profile_doc)s
1063 @return: a deferred None value 1051 @return: a deferred None value
1064 """ 1052 """
1065 1053
1066 def gotIndMemory(data): 1054 def gotIndMemory(data):
1067 d = BlockCipher.encrypt(crypto_key, data_value) 1055 data[data_key] = BlockCipher.encrypt(crypto_key, data_value)
1068 1056 return data.force(data_key)
1069 def cb(cipher):
1070 data[data_key] = cipher
1071 return data.force(data_key)
1072
1073 return d.addCallback(cb)
1074 1057
1075 def done(__): 1058 def done(__):
1076 log.debug( 1059 log.debug(
1077 _("Personal data (%(ns)s, %(key)s) has been successfuly encrypted") 1060 _("Personal data (%(ns)s, %(key)s) has been successfuly encrypted")
1078 % {"ns": C.MEMORY_CRYPTO_NAMESPACE, "key": data_key} 1061 % {"ns": C.MEMORY_CRYPTO_NAMESPACE, "key": data_key}