From e50a442c6f03ba09a800f9999e29e9340b1d45c7 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Thu, 24 Nov 2016 16:57:56 +0100 Subject: [feature] local session service --- src/leap/bitmask/core/_session.py | 70 +++++++++++++++++++++++++++++++++++++++ src/leap/bitmask/core/_web.py | 46 +++++++++++-------------- src/leap/bitmask/core/service.py | 16 +++++++++ 3 files changed, 105 insertions(+), 27 deletions(-) create mode 100644 src/leap/bitmask/core/_session.py diff --git a/src/leap/bitmask/core/_session.py b/src/leap/bitmask/core/_session.py new file mode 100644 index 0000000..24070a8 --- /dev/null +++ b/src/leap/bitmask/core/_session.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# _session.py +# Copyright (C) 2016 LEAP Encryption Acess Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Service for handling the local sessions. +""" + +import binascii +import os + +from twisted.application import service +from twisted.logger import Logger + +from leap.bitmask.hooks import HookableService + + +logger = Logger() + + +class SessionService(HookableService): + + """ + This service holds random local-session tokens, that will be use to protect + the access to the API resources. + + These tokens are different from the (remote) SRP session tokens: the + local-session tokens are ephimeral and generated by the local Bitmask + deamon. + + Right now, they are generated when a soledad instance is successfully + created. This might be subject to further discussion, but this is the + earliest moment in which we can decide if a user should be authenticated + locally: it means that the entered password is able to decrypt the local + store. In this way, we can protect the API resources even in the case that + we don't have connectivity. + """ + + name = 'sessions' + + def __init__(self, basedir, tokens): + service.Service.__init__(self) + self._basedir = basedir + self._tokens = tokens + + def startService(self): + logger.info('starting Session Service') + super(SessionService, self).startService() + + def stopService(self): + pass + + def hook_on_new_soledad_instance(self, **kw): + user = kw['user'] + session_token = binascii.hexlify(os.urandom(10)) + print '---------------------------------------------------' + print "hook on new soledad instance!", user, session_token + self._tokens[user] = session_token diff --git a/src/leap/bitmask/core/_web.py b/src/leap/bitmask/core/_web.py index c0c041e..11a7be1 100644 --- a/src/leap/bitmask/core/_web.py +++ b/src/leap/bitmask/core/_web.py @@ -64,8 +64,7 @@ class TokenDictChecker: credentials.IUsernameHashedPassword) def __init__(self, tokens): - "tokens: a dict-like object mapping usernames to session-tokens" - self.tokens = tokens + self.tokens = tokens def requestAvatarId(self, credentials): username = credentials.username @@ -87,8 +86,8 @@ class HttpPasswordRealm(object): self.resource = resource def requestAvatar(self, user, mind, *interfaces): + # the resource is passed on regardless of user if IResource in interfaces: - # the resource is passed on regardless of user return (IResource, self.resource, lambda: None) raise NotImplementedError() @@ -123,12 +122,9 @@ class WhitelistHTTPAuthSessionWrapper(HTTPAuthSessionWrapper): return HTTPAuthSessionWrapper.render(self, request) - -def protectedResourceFactory(resource, passwords, whitelist): +def protectedResourceFactory(resource, session_tokens, whitelist): realm = HttpPasswordRealm(resource) - # TODO this should have the per-site tokens. - # can put it inside the API Resource object. - checker = PasswordDictChecker(passwords) + checker = TokenDictChecker(session_tokens) resource_portal = portal.Portal(realm, [checker]) credentialFactory = TokenCredentialFactory('localhost') protected_resource = WhitelistHTTPAuthSessionWrapper( @@ -155,7 +151,6 @@ class HTTPDispatcherService(service.Service): '/API/bonafide/user', ) - def __init__(self, core, port=7070, debug=False, onion=False): self._core = core self.port = port @@ -178,33 +173,20 @@ class HTTPDispatcherService(service.Service): 'ui', 'app', 'lib', 'bitmask.js') jsapi = File(os.path.abspath(jspath)) - root = File(webdir) - - # TODO move this to the tests... - DUMMY_PASS = {'user1': 'pass'} - api = Api(CommandDispatcher(self._core)) protected_api = protectedResourceFactory( - api, DUMMY_PASS, self.API_WHITELIST) - root.putChild(u'API', protected_api) + api, self._core.tokens, self.API_WHITELIST) + root = File(webdir) + root.putChild(u'API', protected_api) if not HAS_WEB_UI: root.putChild('bitmask.js', jsapi) - # TODO --- pass requestFactory for header authentication - # so that we remove the setting of the cookie. - - # http://www.tsheffler.com/blog/2011/09/22/twisted-learning-about-cred-and-basicdigest-authentication/#Digest_Authentication factory = Site(root) self.site = factory - if self.onion: - try: - import txtorcon - except ImportError: - log.error('onion is enabled, but could not find txtorcon') - return - self._start_onion_service(factory) + if self.onion and _has_txtorcon(): + self._start_onion_service(factory) else: interface = '127.0.0.1' endpoint = endpoints.TCP4ServerEndpoint( @@ -274,3 +256,13 @@ class Api(Resource): request.setHeader('Content-Type', 'application/json') request.write(response) request.finish() + + +def _has_txtorcon(): + try: + import txtorcon + txtorcon + except ImportError: + log.error('onion is enabled, but could not find txtorcon') + return False + return True diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py index cba6f8d..705dcb5 100644 --- a/src/leap/bitmask/core/service.py +++ b/src/leap/bitmask/core/service.py @@ -31,6 +31,7 @@ from leap.bitmask.core import configurable from leap.bitmask.core import _zmq from leap.bitmask.core import _web from leap.bitmask.core import flags +from leap.bitmask.core import _session from leap.common.events import server as event_server # from leap.vpn import EIPService @@ -61,6 +62,7 @@ class BitmaskBackend(configurable.ConfigurableService): configurable.ConfigurableService.__init__(self, basedir) self.core_commands = BackendCommands(self) + self.tokens = {} def enabled(service): return self.get_config('services', service, False, boolean=True) @@ -69,6 +71,7 @@ class BitmaskBackend(configurable.ConfigurableService): on_start(self.init_events) on_start(self.init_bonafide) + on_start(self.init_sessions) if enabled('mail'): on_start(self._init_mail_services) @@ -103,6 +106,10 @@ class BitmaskBackend(configurable.ConfigurableService): bf.register_hook('on_bonafide_auth', listener='mail') bf.register_hook('on_bonafide_logout', listener='mail') + def init_sessions(self): + sessions = _session.SessionService(self.basedir, self.tokens) + sessions.setServiceParent(self) + def _start_child_service(self, name): logger.debug('starting backend child service: %s' % name) service = self.getServiceNamed(name) @@ -138,6 +145,15 @@ class BitmaskBackend(configurable.ConfigurableService): sol.register_hook( 'on_new_soledad_instance', listener='keymanager') + # XXX this might not be the right place for hooking the sessions. + # If we want to be offline, we need to authenticate them after + # soledad. But this is not valid for the VPN case, + # because we have not decided if soledad is required in that case + # (seemingly not). If only VPN, then we have to return the token + # from the SRP authentication. + sol.register_hook( + 'on_new_soledad_instance', listener='sessions') + def _init_keymanager(self): service = mail_services.KeymanagerService km = self._maybe_init_service( -- cgit v1.2.3