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:
parent
c9f4397000
commit
0d28c03d27
3 changed files with 478 additions and 166 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -88,3 +88,7 @@ class BridgeExceptionNoService(Exception):
|
|||
|
||||
class DatabaseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PasswordError(Exception):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue