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
This commit is contained in:
souliane 2014-05-07 16:02:23 +02:00
commit 0d28c03d27
3 changed files with 478 additions and 166 deletions

View file

@ -40,6 +40,10 @@ class Const(object):
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"

View file

@ -88,3 +88,7 @@ class BridgeExceptionNoService(Exception):
class DatabaseError(Exception):
pass
class PasswordError(Exception):
pass

View file

@ -31,6 +31,7 @@ from sat.core.log import getLogger
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 @@ class SAT(service.Service):
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
def asyncConnect(self, profile_key=C.PROF_KEY_NONE):
"""Connect to jabber server with asynchronous reply
@param profile_key: %(doc_profile)s
@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, 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,85 +225,111 @@ class SAT(service.Service):
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
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)
current.messageProt = xmpp.SatMessageProtocol(self)
current.messageProt.setHandlerParent(current)
current.roster = xmpp.SatRosterProtocol(self)
current.roster.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.identityHandler = xmpp.SatIdentityHandler()
current.identityHandler.setHandlerParent(current)
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))
current.startService()
d = current.getConnectionDeferred()
d.addCallback(lambda dummy: current.roster.got_roster) # we want to be sure that we got the roster
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})
list_d.addCallback(logPluginResults)
return list_d
d.addCallback(pluginsConnection)
return d
self.memory.startProfileSession(profile)
return self.memory.loadIndividualParams(profile).addCallback(afterMemoryInit)
return self._initialised.addCallback(backendInitialised)
@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
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.messageProt = xmpp.SatMessageProtocol(self)
current.messageProt.setHandlerParent(current)
current.roster = xmpp.SatRosterProtocol(self)
current.roster.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.identityHandler = xmpp.SatIdentityHandler()
current.identityHandler.setHandlerParent(current)
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))
current.startService()
yield current.getConnectionDeferred()
yield 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 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.
@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)
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 = 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"""
if not self.isConnected(profile_key):
@ -388,21 +427,24 @@ class SAT(service.Service):
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 @@ class SAT(service.Service):
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 @@ class SAT(service.Service):
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()