diff options
| -rw-r--r-- | docs/core/index.rst | 27 | ||||
| -rw-r--r-- | src/leap/bitmask/core/service.py | 18 | ||||
| -rw-r--r-- | src/leap/bitmask/core/web/_auth.py | 7 | ||||
| -rw-r--r-- | src/leap/bitmask/core/web/api.py | 11 | ||||
| -rw-r--r-- | src/leap/bitmask/core/web/service.py | 20 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/app.py | 10 | ||||
| -rw-r--r-- | tests/unit/core/test_web_api.py | 5 | 
7 files changed, 73 insertions, 25 deletions
| diff --git a/docs/core/index.rst b/docs/core/index.rst index d03dd727..c7fb1780 100644 --- a/docs/core/index.rst +++ b/docs/core/index.rst @@ -24,8 +24,31 @@ throught a REST API. In bitmaskd.cfg::    [services]    web = True -API Authentication -================== + +Global API Authentication +========================= + +To avoid some kind of attacks, the Bitmask API is protected by a global +authentication token. + +The JS API receives this value when the initial entrypoint is loaded for the +first time, in the anchor part of the url. + +To authenticate any request to the API, the ``X-Bitmask-Auth`` header has to be +added to it, set to the single value that is initialized during the bitmask +deaemon startup:: + +  curl -X POST http://localhost:7070/API/mail/status +  unauthorized:bad auth token + +  curl -X POST http://localhost:7070/API/mail/status -H 'X-Bitmask-Auth: fae20706aa4f4f98ac0e67996787a370' +  {"result": {"status": "on", "childrenStatus": {"smtp": {"status": "on", "error": null}, "imap": {"status": "on", "error": null}}, "error": null}, "error": null} + +This token can be found in ``.config/leap/authtoken`` + + +API Authentication (this section not implemented yet) +======================================================  By default, the resources in the API are protected by an authentication token. diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py index 902bfa6b..c06a5343 100644 --- a/src/leap/bitmask/core/service.py +++ b/src/leap/bitmask/core/service.py @@ -18,6 +18,8 @@  Bitmask-core Service.  """  import json +import os +import uuid  try:      import resource  except ImportError: @@ -62,6 +64,16 @@ class BitmaskBackend(configurable.ConfigurableService):          configurable.ConfigurableService.__init__(self, basedir)          self.core_commands = BackendCommands(self) + +        # The global token is used for authenticating some of the channels that +        # expose the dispatcher. For the moment being, this is the REST API. +        self.global_tokens = [uuid.uuid4().hex] +        logger.info('Global token: {0}'.format(self.global_tokens[0])) +        self._touch_token_file() + +        # These tokens are user-session tokens. Implemented and rolled back, +        # unused for now. If we don't move forward with user-session tokens on +        # top of the global app token, this should be removed.          self.tokens = {}          def enabled(service): @@ -89,6 +101,12 @@ class BitmaskBackend(configurable.ConfigurableService):          if enabled('websockets'):              on_start(self._init_websockets) +    def _touch_token_file(self): +        path = os.path.join(self.basedir, 'authtoken') +        with open(path, 'w') as f: +            f.write(self.global_tokens[0]) +        os.chmod(path, 0600) +      def init_events(self):          event_server.ensure_server() diff --git a/src/leap/bitmask/core/web/_auth.py b/src/leap/bitmask/core/web/_auth.py index 2747fae8..aa6aeb9b 100644 --- a/src/leap/bitmask/core/web/_auth.py +++ b/src/leap/bitmask/core/web/_auth.py @@ -6,6 +6,7 @@ from twisted.web.guard import HTTPAuthSessionWrapper, BasicCredentialFactory  from twisted.web.resource import IResource +# Deprecate if the user-session tokens are finally not used.  class TokenCredentialFactory(BasicCredentialFactory):      scheme = 'token' @@ -37,11 +38,11 @@ class WhitelistHTTPAuthSessionWrapper(HTTPAuthSessionWrapper):          return HTTPAuthSessionWrapper.render(self, request) -def protectedResourceFactory(resource, session_tokens, whitelist): +def protectedResourceFactory(resource, tokens, whitelist):      realm = HttpPasswordRealm(resource) -    checker = TokenDictChecker(session_tokens) -    resource_portal = portal.Portal(realm, [checker]) +    checker = TokenDictChecker(tokens)      credentialFactory = TokenCredentialFactory('localhost') +    resource_portal = portal.Portal(realm, [checker])      protected_resource = WhitelistHTTPAuthSessionWrapper(          resource_portal, [credentialFactory],          whitelist=whitelist) diff --git a/src/leap/bitmask/core/web/api.py b/src/leap/bitmask/core/web/api.py index d31afa50..01c65bae 100644 --- a/src/leap/bitmask/core/web/api.py +++ b/src/leap/bitmask/core/web/api.py @@ -11,11 +11,20 @@ class Api(Resource):      isLeaf = True -    def __init__(self, dispatcher): +    def __init__(self, dispatcher, global_tokens):          Resource.__init__(self)          self.dispatcher = dispatcher +        self.global_tokens = global_tokens      def render_POST(self, request): +        token = request.getHeader('x-bitmask-auth') +        if not token: +            request.setResponseCode(401) +            return 'unauthorized: no app token' +        elif token.strip() not in self.global_tokens: +            request.setResponseCode(401) +            return 'unauthorized: bad app token' +          command = request.uri.split('/')[2:]          params = request.content.getvalue()          if params: diff --git a/src/leap/bitmask/core/web/service.py b/src/leap/bitmask/core/web/service.py index c1d839e8..2b8a7343 100644 --- a/src/leap/bitmask/core/web/service.py +++ b/src/leap/bitmask/core/web/service.py @@ -61,11 +61,6 @@ class HTTPDispatcherService(service.Service):      API_WHITELIST = (          '/API/core/version',          '/API/core/stats', -        '/API/bonafide/user/create', -        '/API/bonafide/user/authenticate', -        '/API/bonafide/provider/list', -        '/API/bonafide/provider/create', -        '/API/bonafide/provider/read',      )      def __init__(self, core, port=7070, debug=False, onion=False): @@ -76,7 +71,6 @@ class HTTPDispatcherService(service.Service):          self.uri = ''      def startService(self): -        # TODO refactor this, too long----------------------------------------          if HAS_WEB_UI:              webdir = os.path.abspath(                  pkg_resources.resource_filename('leap.bitmask_js', 'public')) @@ -91,18 +85,16 @@ class HTTPDispatcherService(service.Service):                  'ui', 'app', 'lib', 'bitmask.js')              jsapi = File(os.path.abspath(jspath)) -        api = Api(CommandDispatcher(self._core)) -        # protected_api = protectedResourceFactory( -        #    api, self._core.tokens, self.API_WHITELIST) +        api = Api(CommandDispatcher(self._core), self._core.global_tokens)          root = File(webdir) +        root.putChild(u'API', api) -        # FIXME -- switching off the protected api, due to -        # https://0xacab.org/leap/bitmask-dev/issues/9 +        # XXX remove it we don't bring session tokens again +        # protected_api = protectedResourceFactory( +        # api, self._core.global_tokens, self.API_WHITELIST)          # root.putChild(u'API', protected_api) -        # ------------------------------------------------- -        root.putChild(u'API', api)          if not HAS_WEB_UI:              root.putChild('bitmask.js', jsapi) @@ -110,7 +102,7 @@ class HTTPDispatcherService(service.Service):          self.site = factory          if self.onion and _has_txtorcon(): -                self._start_onion_service(factory) +            self._start_onion_service(factory)          else:              interface = '127.0.0.1'              endpoint = endpoints.TCP4ServerEndpoint( diff --git a/src/leap/bitmask/gui/app.py b/src/leap/bitmask/gui/app.py index ce9fc880..14025afc 100644 --- a/src/leap/bitmask/gui/app.py +++ b/src/leap/bitmask/gui/app.py @@ -30,8 +30,8 @@ from functools import partial  from multiprocessing import Process  from leap.bitmask.core.launcher import run_bitmaskd, pid -  from leap.bitmask.gui import app_rc +from leap.common.config import get_path_prefix  if platform.system() == 'Windows': @@ -51,7 +51,7 @@ else:      from PyQt5.QtCore import QSize -BITMASK_URI = 'http://localhost:7070' +BITMASK_URI = 'http://localhost:7070/'  IS_WIN = platform.system() == "Windows"  DEBUG = os.environ.get("DEBUG", False) @@ -100,7 +100,11 @@ class BrowserWindow(QDialog):          self.closing = False      def load_app(self): -        self.view.load(QtCore.QUrl(BITMASK_URI)) +        path = os.path.join(get_path_prefix(), 'leap', 'authtoken') +        global_token = open(path).read().strip() +        anchored_uri = BITMASK_URI + 'index.html#' + global_token +        print('[bitmask] opening Browser with {0}'.format(anchored_uri)) +        self.view.load(QtCore.QUrl(anchored_uri))      def shutdown(self, *args):          if self.closing: diff --git a/tests/unit/core/test_web_api.py b/tests/unit/core/test_web_api.py index 5b51869f..10356f02 100644 --- a/tests/unit/core/test_web_api.py +++ b/tests/unit/core/test_web_api.py @@ -131,7 +131,7 @@ class RESTApiTests(unittest.TestCase):      def setUp(self):          dispatcher = dummyDispatcherFactory() -        api = web.api.Api(dispatcher) +        api = web.api.Api(dispatcher, ['aaa'])          root = resource.Resource()          root.putChild(b"API", api)          plainSite = Site(root) @@ -205,7 +205,8 @@ class RESTApiTests(unittest.TestCase):          uri = networkString("http://127.0.0.1:%d/API/%s" % (              self.plainPortno, path))          return client.getPage( -            uri, method=method, timeout=1, postdata=postdata) +            uri, method=method, timeout=1, postdata=postdata, +            headers={'X-Bitmask-Auth': 'aaa'})      def assertCall(self, returned, expected):          data = json.loads(returned) | 
