From 22eec36ff81ae2ec2b924087ed6253894b92278a Mon Sep 17 00:00:00 2001
From: "Kali Kaneko (leap communications)" <kali@leap.se>
Date: Tue, 30 Aug 2016 10:53:32 -0400
Subject: [pkg] initial migration of bitmask.{core,cli}

---
 src/leap/bitmask/cli/__init__.py       |   0
 src/leap/bitmask/cli/bitmask_cli.py    | 119 ++++++
 src/leap/bitmask/cli/command.py        | 120 ++++++
 src/leap/bitmask/cli/eip.py            |  37 ++
 src/leap/bitmask/cli/keys.py           | 124 ++++++
 src/leap/bitmask/cli/mail.py           |  40 ++
 src/leap/bitmask/cli/user.py           |  76 ++++
 src/leap/bitmask/core/__init__.py      |   2 +
 src/leap/bitmask/core/_zmq.py          |  68 ++++
 src/leap/bitmask/core/api.py           |  54 +++
 src/leap/bitmask/core/bitmaskd.tac     |  11 +
 src/leap/bitmask/core/configurable.py  | 106 +++++
 src/leap/bitmask/core/dispatcher.py    | 328 ++++++++++++++++
 src/leap/bitmask/core/dummy.py         |  80 ++++
 src/leap/bitmask/core/flags.py         |   1 +
 src/leap/bitmask/core/launcher.py      |  47 +++
 src/leap/bitmask/core/mail_services.py | 690 +++++++++++++++++++++++++++++++++
 src/leap/bitmask/core/service.py       | 209 ++++++++++
 src/leap/bitmask/core/uuid_map.py      | 115 ++++++
 src/leap/bitmask/core/web/__init__.py  |   0
 src/leap/bitmask/core/web/index.html   |  70 ++++
 src/leap/bitmask/core/web/root.py      |   0
 src/leap/bitmask/core/websocket.py     |  98 +++++
 23 files changed, 2395 insertions(+)
 create mode 100644 src/leap/bitmask/cli/__init__.py
 create mode 100755 src/leap/bitmask/cli/bitmask_cli.py
 create mode 100644 src/leap/bitmask/cli/command.py
 create mode 100644 src/leap/bitmask/cli/eip.py
 create mode 100644 src/leap/bitmask/cli/keys.py
 create mode 100644 src/leap/bitmask/cli/mail.py
 create mode 100644 src/leap/bitmask/cli/user.py
 create mode 100644 src/leap/bitmask/core/__init__.py
 create mode 100644 src/leap/bitmask/core/_zmq.py
 create mode 100644 src/leap/bitmask/core/api.py
 create mode 100644 src/leap/bitmask/core/bitmaskd.tac
 create mode 100644 src/leap/bitmask/core/configurable.py
 create mode 100644 src/leap/bitmask/core/dispatcher.py
 create mode 100644 src/leap/bitmask/core/dummy.py
 create mode 100644 src/leap/bitmask/core/flags.py
 create mode 100644 src/leap/bitmask/core/launcher.py
 create mode 100644 src/leap/bitmask/core/mail_services.py
 create mode 100644 src/leap/bitmask/core/service.py
 create mode 100644 src/leap/bitmask/core/uuid_map.py
 create mode 100644 src/leap/bitmask/core/web/__init__.py
 create mode 100644 src/leap/bitmask/core/web/index.html
 create mode 100644 src/leap/bitmask/core/web/root.py
 create mode 100644 src/leap/bitmask/core/websocket.py

(limited to 'src')

diff --git a/src/leap/bitmask/cli/__init__.py b/src/leap/bitmask/cli/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/leap/bitmask/cli/bitmask_cli.py b/src/leap/bitmask/cli/bitmask_cli.py
new file mode 100755
index 00000000..1f104c9c
--- /dev/null
+++ b/src/leap/bitmask/cli/bitmask_cli.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# bitmask_cli
+# Copyright (C) 2015, 2016 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: zmq client.
+"""
+import json
+import sys
+
+from colorama import Fore
+from twisted.internet import reactor, defer
+
+from leap.bitmask.cli.eip import Eip
+from leap.bitmask.cli.keys import Keys
+from leap.bitmask.cli.mail import Mail
+from leap.bitmask.cli import command
+from leap.bitmask.cli.user import User
+
+
+class BitmaskCLI(command.Command):
+    usage = '''bitmaskctl <command> [<args>]
+
+Controls the Bitmask application.
+
+SERVICE COMMANDS:
+
+  user       Handles Bitmask accounts
+  mail       Bitmask Encrypted Mail
+  eip        Encrypted Internet Proxy
+  keys       Bitmask Keymanager
+
+GENERAL COMMANDS:
+
+  version    prints version number and exit
+  launch     launch the Bitmask backend daemon
+  shutdown   shutdown Bitmask backend daemon
+  status     displays general status about the running Bitmask services
+  stats      show some debug info about bitmask-core
+  help       show this help message
+
+'''
+    epilog = ("Use 'bitmaskctl <command> help' to learn more "
+              "about each command.")
+    commands = ['shutdown', 'stats']
+
+    def user(self, raw_args):
+        user = User()
+        return user.execute(raw_args)
+
+    def mail(self, raw_args):
+        mail = Mail()
+        return mail.execute(raw_args)
+
+    def eip(self, raw_args):
+        eip = Eip()
+        return eip.execute(raw_args)
+
+    def keys(self, raw_args):
+        keys = Keys()
+        return keys.execute(raw_args)
+
+    # Single commands
+
+    def launch(self, raw_args):
+        # XXX careful! Should see if the process in PID is running,
+        # avoid launching again.
+        import commands
+        commands.getoutput('bitmaskd')
+        return defer.succeed(None)
+
+    def version(self, raw_args):
+        print(Fore.GREEN + 'bitmaskctl:  ' + Fore.RESET + '0.0.1')
+        self.data = ['version']
+        return self._send(printer=self._print_version)
+
+    def _print_version(self, version):
+        corever = version['version_core']
+        print(Fore.GREEN + 'bitmask_core: ' + Fore.RESET + corever)
+
+    def status(self, raw_args):
+        self.data = ['status']
+        return self._send(printer=self._print_status)
+
+    def _print_status(self, status):
+        statusdict = json.loads(status)
+        for key, value in statusdict.items():
+            color = Fore.GREEN
+            if value == 'stopped':
+                color = Fore.RED
+            print(key.ljust(10) + ': ' + color +
+                  value + Fore.RESET)
+
+
+def execute():
+    cli = BitmaskCLI()
+    d = cli.execute(sys.argv[1:])
+    d.addCallback(lambda _: reactor.stop())
+
+
+def main():
+    reactor.callWhenRunning(reactor.callLater, 0, execute)
+    reactor.run()
+
+if __name__ == "__main__":
+    main()
diff --git a/src/leap/bitmask/cli/command.py b/src/leap/bitmask/cli/command.py
new file mode 100644
index 00000000..36552c03
--- /dev/null
+++ b/src/leap/bitmask/cli/command.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+# sender
+# Copyright (C) 2016 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: zmq sender.
+"""
+import argparse
+import json
+import sys
+
+from colorama import init as color_init
+from colorama import Fore
+from twisted.internet import defer
+from txzmq import ZmqEndpoint, ZmqEndpointType
+from txzmq import ZmqFactory, ZmqREQConnection
+from txzmq import ZmqRequestTimeoutError
+
+from leap.bitmask.core import ENDPOINT
+
+
+appname = 'bitmaskctl'
+
+
+def _print_result(result):
+    print Fore.GREEN + '%s' % result + Fore.RESET
+
+
+def default_dict_printer(result):
+    for key, value in result.items():
+        if value is None:
+            value = str(value)
+        print(Fore.RESET + key.ljust(10) + Fore.GREEN + value + Fore.RESET)
+
+
+class Command(object):
+    """A generic command dispatcher.
+    Any command in the class attribute `commands` will be dispached and
+    represented with a generic printer."""
+    service = ''
+    usage = '''{name} <subcommand>'''.format(name=appname)
+    epilog = ("Use bitmaskctl <subcommand> --help' to learn more "
+              "about each command.")
+    commands = []
+
+    def __init__(self):
+        color_init()
+        zf = ZmqFactory()
+        e = ZmqEndpoint(ZmqEndpointType.connect, ENDPOINT)
+        self._conn = ZmqREQConnection(zf, e)
+
+        self.data = []
+        if self.service:
+            self.data = [self.service]
+
+    def execute(self, raw_args):
+        self.parser = argparse.ArgumentParser(usage=self.usage,
+                                              epilog=self.epilog)
+        self.parser.add_argument('command', help='Subcommand to run')
+        try:
+            args = self.parser.parse_args(raw_args[0:1])
+        except SystemExit:
+            return defer.succeed(None)
+
+        # if command is in the default list, send the bare command
+        # and use the default printer
+        if args.command in self.commands:
+            self.data += [args.command]
+            return self._send(printer=default_dict_printer)
+
+        elif (args.command == 'execute' or
+                args.command.startswith('_') or
+                not hasattr(self, args.command)):
+            print 'Unrecognized command'
+            return self.help([])
+
+        try:
+            # use dispatch pattern to invoke method with same name
+            return getattr(self, args.command)(raw_args[1:])
+        except SystemExit:
+            return defer.succeed(None)
+
+    def help(self, raw_args):
+        self.parser.print_help()
+        return defer.succeed(None)
+
+    def _send(self, printer=_print_result):
+        d = self._conn.sendMsg(*self.data, timeout=60)
+        d.addCallback(self._check_err, printer)
+        d.addErrback(self._timeout_handler)
+        return d
+
+    def _error(self, msg):
+        print Fore.RED + "[!] %s" % msg + Fore.RESET
+        sys.exit(1)
+
+    def _check_err(self, stuff, printer):
+        obj = json.loads(stuff[0])
+        if not obj['error']:
+            return printer(obj['result'])
+        else:
+            print Fore.RED + 'ERROR:' + '%s' % obj['error'] + Fore.RESET
+
+    def _timeout_handler(self, failure):
+        # TODO ---- could try to launch the bitmask daemon here and retry
+        if failure.trap(ZmqRequestTimeoutError) == ZmqRequestTimeoutError:
+            print (Fore.RED + "[ERROR] Timeout contacting the bitmask daemon. "
+                   "Is it running?" + Fore.RESET)
diff --git a/src/leap/bitmask/cli/eip.py b/src/leap/bitmask/cli/eip.py
new file mode 100644
index 00000000..eac8682a
--- /dev/null
+++ b/src/leap/bitmask/cli/eip.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# eip
+# Copyright (C) 2016 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: eip
+"""
+from leap.bitmask.cli import command
+
+
+class Eip(command.Command):
+    service = 'eip'
+    usage = '''{name} eip <subcommand>
+
+Bitmask Encrypted Internet Service
+
+SUBCOMMANDS:
+
+   start      Start service
+   stop       Stop service
+   status     Display status about service
+
+'''.format(name=command.appname)
+
+    commands = ['start', 'stop', 'status']
diff --git a/src/leap/bitmask/cli/keys.py b/src/leap/bitmask/cli/keys.py
new file mode 100644
index 00000000..d74c40a8
--- /dev/null
+++ b/src/leap/bitmask/cli/keys.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+# keys
+# Copyright (C) 2016 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: keys
+"""
+import argparse
+import sys
+
+from colorama import Fore
+
+from leap.bitmask.cli import command
+from leap.keymanager.validation import ValidationLevels
+
+
+class Keys(command.Command):
+    service = 'keys'
+    usage = '''{name} keys <subcommand>
+
+Bitmask Keymanager management service
+
+SUBCOMMANDS:
+
+   list       List all known keys
+   export     Export a given key
+   insert     Insert a key to the key storage
+   delete     Delete a key from the key storage
+'''.format(name=command.appname)
+
+    def list(self, raw_args):
+        parser = argparse.ArgumentParser(
+            description='Bitmask list keys',
+            prog='%s %s %s' % tuple(sys.argv[:3]))
+        parser.add_argument('--private', action='store_true',
+                            help='Use private keys (by default uses public)')
+        subargs = parser.parse_args(raw_args)
+
+        self.data += ['list']
+        if subargs.private:
+            self.data += ['private']
+        else:
+            self.data += ['public']
+
+        return self._send(self._print_key_list)
+
+    def export(self, raw_args):
+        parser = argparse.ArgumentParser(
+            description='Bitmask export key',
+            prog='%s %s %s' % tuple(sys.argv[:3]))
+        parser.add_argument('--private', action='store_true',
+                            help='Use private keys (by default uses public)')
+        parser.add_argument('address', nargs=1,
+                            help='email address of the key')
+        subargs = parser.parse_args(raw_args)
+        self.data += ['export', subargs.address[0]]
+
+        return self._send(self._print_key)
+
+    def insert(self, raw_args):
+        parser = argparse.ArgumentParser(
+            description='Bitmask import key',
+            prog='%s %s %s' % tuple(sys.argv[:3]))
+        parser.add_argument('--validation', choices=list(ValidationLevels),
+                            default='Fingerprint',
+                            help='Validation level for the key')
+        parser.add_argument('file', nargs=1,
+                            help='file where the key is stored')
+        parser.add_argument('address', nargs=1,
+                            help='email address of the key')
+        subargs = parser.parse_args(raw_args)
+
+        with open(subargs.file[0], 'r') as keyfile:
+            rawkey = keyfile.read()
+        self.data += ['insert', subargs.address[0], subargs.validation,
+                      rawkey]
+
+        return self._send(self._print_key)
+
+    def delete(self, raw_args):
+        parser = argparse.ArgumentParser(
+            description='Bitmask delete key',
+            prog='%s %s %s' % tuple(sys.argv[:3]))
+        parser.add_argument('--private', action='store_true',
+                            help='Use private keys (by default uses public)')
+        parser.add_argument('address', nargs=1,
+                            help='email address of the key')
+        subargs = parser.parse_args(raw_args)
+        self.data += ['delete', subargs.address[0]]
+
+        return self._send()
+
+    def _print_key_list(self, keys):
+        for key in keys:
+            print(Fore.GREEN +
+                  key["fingerprint"] + " " + key['address'] +
+                  Fore.RESET)
+
+    def _print_key(self, key):
+        print(Fore.GREEN)
+        print("Uids:       " + ', '.join(key['uids']))
+        print("Fingerprint:" + key['fingerprint'])
+        print("Length:     " + str(key['length']))
+        print("Expiration: " + key['expiry_date'])
+        print("Validation: " + key['validation'])
+        print("Used:       " + "sig:" +
+              str(key['sign_used']) + ", encr:" +
+              str(key['encr_used']))
+        print("Refreshed:   " + key['refreshed_at'])
+        print(Fore.RESET)
+        print("")
+        print(key['key_data'])
diff --git a/src/leap/bitmask/cli/mail.py b/src/leap/bitmask/cli/mail.py
new file mode 100644
index 00000000..f0fa9722
--- /dev/null
+++ b/src/leap/bitmask/cli/mail.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# mail
+# Copyright (C) 2016 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: mail
+"""
+from leap.bitmask.cli import command
+
+
+class Mail(command.Command):
+    service = 'mail'
+    usage = '''{name} mail <subcommand>
+
+Bitmask Encrypted Email Service
+
+SUBCOMMANDS:
+
+   enable               Start service
+   disable              Stop service
+   status               Display status about service
+   get_token            Returns token for the mail service
+   get_smtp_certificate Downloads a new smtp certificate
+
+'''.format(name=command.appname)
+
+    commands = ['enable', 'disable', 'status', 'get_token',
+                'get_smtp_certificate']
diff --git a/src/leap/bitmask/cli/user.py b/src/leap/bitmask/cli/user.py
new file mode 100644
index 00000000..dccfc7d5
--- /dev/null
+++ b/src/leap/bitmask/cli/user.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# user
+# Copyright (C) 2016 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: user
+"""
+import argparse
+import getpass
+import sys
+
+from leap.bitmask.cli import command
+
+
+class User(command.Command):
+    service = 'user'
+    usage = '''{name} user <subcommand>
+
+Bitmask account service
+
+SUBCOMMANDS:
+
+   create     Registers new user, if possible
+   auth       Logs in against the provider
+   logout     Ends any active session with the provider
+   active     Shows the active user, if any
+
+'''.format(name=command.appname)
+
+    commands = ['active']
+
+    def create(self, raw_args):
+        username = self.username(raw_args)
+        passwd = getpass.getpass()
+        self.data += ['signup', username, passwd]
+        return self._send(printer=command.default_dict_printer)
+
+    def auth(self, raw_args):
+        username = self.username(raw_args)
+        passwd = getpass.getpass()
+        self.data += ['authenticate', username, passwd]
+        return self._send(printer=command.default_dict_printer)
+
+    def logout(self, raw_args):
+        username = self.username(raw_args)
+        self.data += ['logout', username]
+        return self._send(printer=command.default_dict_printer)
+
+    def username(self, raw_args):
+        args = tuple([command.appname] + sys.argv[1:3])
+        parser = argparse.ArgumentParser(
+            description='Bitmask user',
+            prog='%s %s %s' % args)
+        parser.add_argument('username', nargs=1,
+                            help='username ID, in the form <user@example.org>')
+        subargs = parser.parse_args(raw_args)
+
+        username = subargs.username[0]
+        if not username:
+            self._error("Missing username ID but needed for this command")
+        if '@' not in username:
+            self._error("Username ID must be in the form <user@example.org>")
+
+        return username
diff --git a/src/leap/bitmask/core/__init__.py b/src/leap/bitmask/core/__init__.py
new file mode 100644
index 00000000..bda4b8d0
--- /dev/null
+++ b/src/leap/bitmask/core/__init__.py
@@ -0,0 +1,2 @@
+APPNAME = "bitmask.core"
+ENDPOINT = "ipc:///tmp/%s.sock" % APPNAME
diff --git a/src/leap/bitmask/core/_zmq.py b/src/leap/bitmask/core/_zmq.py
new file mode 100644
index 00000000..a656fc65
--- /dev/null
+++ b/src/leap/bitmask/core/_zmq.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# _zmq.py
+# Copyright (C) 2015 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+ZMQ REQ-REP Dispatcher.
+"""
+
+from twisted.application import service
+from twisted.internet import reactor
+from twisted.python import log
+
+from txzmq import ZmqEndpoint, ZmqEndpointType
+from txzmq import ZmqFactory, ZmqREPConnection
+
+from leap.bitmask.core import ENDPOINT
+from leap.bitmask.core.dispatcher import CommandDispatcher
+
+
+class ZMQServerService(service.Service):
+
+    def __init__(self, core):
+        self._core = core
+
+    def startService(self):
+        zf = ZmqFactory()
+        e = ZmqEndpoint(ZmqEndpointType.bind, ENDPOINT)
+
+        self._conn = _DispatcherREPConnection(zf, e, self._core)
+        reactor.callWhenRunning(self._conn.do_greet)
+        service.Service.startService(self)
+
+    def stopService(self):
+        service.Service.stopService(self)
+
+
+class _DispatcherREPConnection(ZmqREPConnection):
+
+    def __init__(self, zf, e, core):
+        ZmqREPConnection.__init__(self, zf, e)
+        self.dispatcher = CommandDispatcher(core)
+
+    def gotMessage(self, msgId, *parts):
+
+        r = self.dispatcher.dispatch(parts)
+        r.addCallback(self.defer_reply, msgId)
+
+    def defer_reply(self, response, msgId):
+        reactor.callLater(0, self.reply, msgId, str(response))
+
+    def log_err(self, failure, msgId):
+        log.err(failure)
+        self.defer_reply("ERROR: %r" % failure, msgId)
+
+    def do_greet(self):
+        log.msg('starting ZMQ dispatcher')
diff --git a/src/leap/bitmask/core/api.py b/src/leap/bitmask/core/api.py
new file mode 100644
index 00000000..9f3725dc
--- /dev/null
+++ b/src/leap/bitmask/core/api.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# api.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 <http://www.gnu.org/licenses/>.
+"""
+Registry for the public API for the Bitmask Backend.
+"""
+from collections import OrderedDict
+
+registry = OrderedDict()
+
+
+class APICommand(type):
+    """
+    A metaclass to keep a global registry of all the methods that compose the
+    public API for the Bitmask Backend.
+    """
+    def __init__(cls, name, bases, attrs):
+        for key, val in attrs.iteritems():
+            properties = getattr(val, 'register', None)
+            label = getattr(cls, 'label', None)
+            if label:
+                name = label
+            if properties is not None:
+                registry['%s.%s' % (name, key)] = properties
+
+
+def register_method(*args):
+    """
+    This method gathers info about all the methods that are supposed to
+    compose the public API to communicate with the backend.
+
+    It sets up a register property for any method that uses it.
+    A type annotation is supposed to be in this property.
+    The APICommand metaclass collects these properties of the methods and
+    stores them in the global api_registry object, where they can be
+    introspected at runtime.
+    """
+    def decorator(f):
+        f.register = tuple(args)
+        return f
+    return decorator
diff --git a/src/leap/bitmask/core/bitmaskd.tac b/src/leap/bitmask/core/bitmaskd.tac
new file mode 100644
index 00000000..3c9b1d8b
--- /dev/null
+++ b/src/leap/bitmask/core/bitmaskd.tac
@@ -0,0 +1,11 @@
+# Service composition for bitmask-core.
+# Run as: twistd -n -y bitmaskd.tac
+#
+from twisted.application import service
+
+from leap.bitmask.core.service import BitmaskBackend
+
+
+bb = BitmaskBackend()
+application = service.Application("bitmaskd")
+bb.setServiceParent(application)
diff --git a/src/leap/bitmask/core/configurable.py b/src/leap/bitmask/core/configurable.py
new file mode 100644
index 00000000..8e33de95
--- /dev/null
+++ b/src/leap/bitmask/core/configurable.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+# configurable.py
+# Copyright (C) 2015, 2016 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+Configurable Backend for Bitmask Service.
+"""
+import ConfigParser
+import os
+
+from twisted.application import service
+
+from leap.common import files
+from leap.common.config import get_path_prefix
+
+
+DEFAULT_BASEDIR = os.path.join(get_path_prefix(), 'leap')
+
+
+class MissingConfigEntry(Exception):
+    """
+    A required config entry was not found.
+    """
+
+
+class ConfigurableService(service.MultiService):
+
+    config_file = u"bitmaskd.cfg"
+    service_names = ('mail', 'eip', 'zmq', 'web')
+
+    def __init__(self, basedir=DEFAULT_BASEDIR):
+        service.MultiService.__init__(self)
+
+        path = os.path.abspath(os.path.expanduser(basedir))
+        if not os.path.isdir(path):
+            files.mkdir_p(path)
+        self.basedir = path
+
+        # creates self.config
+        self.read_config()
+
+    def get_config(self, section, option, default=None, boolean=False):
+        try:
+            if boolean:
+                return self.config.getboolean(section, option)
+
+            item = self.config.get(section, option)
+            return item
+
+        except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
+            if default is None:
+                fn = self._get_config_path()
+                raise MissingConfigEntry("%s is missing the [%s]%s entry"
+                                         % fn, section, option)
+            return default
+
+    def set_config(self, section, option, value):
+        if not self.config.has_section(section):
+            self.config.add_section(section)
+        self.config.set(section, option, value)
+        self.save_config()
+        self.read_config()
+        assert self.config.get(section, option) == value
+
+    def read_config(self):
+        self.config = ConfigParser.SafeConfigParser()
+        bitmaskd_cfg = self._get_config_path()
+
+        if not os.path.isfile(bitmaskd_cfg):
+            self._create_default_config(bitmaskd_cfg)
+
+        with open(bitmaskd_cfg, "rb") as f:
+            self.config.readfp(f)
+
+    def save_config(self):
+        bitmaskd_cfg = self._get_config_path()
+        with open(bitmaskd_cfg, 'wb') as f:
+            self.config.write(f)
+
+    def _create_default_config(self, path):
+        with open(path, 'w') as outf:
+            outf.write(DEFAULT_CONFIG)
+
+    def _get_config_path(self):
+        return os.path.join(self.basedir, self.config_file)
+
+
+DEFAULT_CONFIG = """
+[services]
+mail = True
+eip = True
+zmq = True
+web = False
+"""
diff --git a/src/leap/bitmask/core/dispatcher.py b/src/leap/bitmask/core/dispatcher.py
new file mode 100644
index 00000000..e81cad62
--- /dev/null
+++ b/src/leap/bitmask/core/dispatcher.py
@@ -0,0 +1,328 @@
+# -*- coding: utf-8 -*-
+# dispatcher.py
+# Copyright (C) 2016 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+Command dispatcher.
+"""
+import json
+
+from twisted.internet import defer
+from twisted.python import failure, log
+
+from .api import APICommand, register_method
+
+
+class SubCommand(object):
+
+    __metaclass__ = APICommand
+
+    def dispatch(self, service, *parts, **kw):
+        subcmd = parts[1]
+
+        _method = getattr(self, 'do_' + subcmd.upper(), None)
+        if not _method:
+            raise RuntimeError('No such subcommand')
+        return defer.maybeDeferred(_method, service, *parts, **kw)
+
+
+class UserCmd(SubCommand):
+
+    label = 'user'
+
+    @register_method("{'srp_token': unicode, 'uuid': unicode}")
+    def do_AUTHENTICATE(self, bonafide, *parts):
+        user, password = parts[2], parts[3]
+        d = defer.maybeDeferred(bonafide.do_authenticate, user, password)
+        return d
+
+    @register_method("{'signup': 'ok', 'user': str}")
+    def do_SIGNUP(self, bonafide, *parts):
+        user, password = parts[2], parts[3]
+        d = defer.maybeDeferred(bonafide.do_signup, user, password)
+        return d
+
+    @register_method("{'logout': 'ok'}")
+    def do_LOGOUT(self, bonafide, *parts):
+        user = parts[2]
+        d = defer.maybeDeferred(bonafide.do_logout, user)
+        return d
+
+    @register_method('str')
+    def do_ACTIVE(self, bonafide, *parts):
+        d = defer.maybeDeferred(bonafide.do_get_active_user)
+        return d
+
+
+class EIPCmd(SubCommand):
+
+    label = 'eip'
+
+    @register_method('dict')
+    def do_ENABLE(self, service, *parts):
+        d = service.do_enable_service(self.label)
+        return d
+
+    @register_method('dict')
+    def do_DISABLE(self, service, *parts):
+        d = service.do_disable_service(self.label)
+        return d
+
+    @register_method('dict')
+    def do_STATUS(self, eip, *parts):
+        d = eip.do_status()
+        return d
+
+    @register_method('dict')
+    def do_START(self, eip, *parts):
+        # TODO --- attempt to get active provider
+        # TODO or catch the exception and send error
+        provider = parts[2]
+        d = eip.do_start(provider)
+        return d
+
+    @register_method('dict')
+    def do_STOP(self, eip, *parts):
+        d = eip.do_stop()
+        return d
+
+
+class MailCmd(SubCommand):
+
+    label = 'mail'
+
+    @register_method('dict')
+    def do_ENABLE(self, service, *parts, **kw):
+        # FIXME -- service doesn't have this method
+        d = service.do_enable_service(self.label)
+        return d
+
+    @register_method('dict')
+    def do_DISABLE(self, service, *parts, **kw):
+        d = service.do_disable_service(self.label)
+        return d
+
+    @register_method('dict')
+    def do_STATUS(self, mail, *parts, **kw):
+        d = mail.do_status()
+        return d
+
+    @register_method('dict')
+    def do_GET_TOKEN(self, mail, *parts, **kw):
+        d = mail.get_token()
+        return d
+
+    @register_method('dict')
+    def do_GET_SMTP_CERTIFICATE(self, mail, *parts, **kw):
+        # TODO move to mail service
+        # TODO should ask for confirmation? like --force or something,
+        # if we already have a valid one. or better just refuse if cert
+        # exists.
+        # TODO how should we pass the userid??
+        # - Keep an 'active' user in bonafide (last authenticated)
+        # (doing it now)
+        # - Get active user from Mail Service (maybe preferred?)
+        # - Have a command/method to set 'active' user.
+
+        @defer.inlineCallbacks
+        def save_cert(cert_data):
+            userid, cert_str = cert_data
+            cert_path = yield mail.do_get_smtp_cert_path(userid)
+            with open(cert_path, 'w') as outf:
+                outf.write(cert_str)
+            defer.returnValue('certificate saved to %s' % cert_path)
+
+        bonafide = kw['bonafide']
+        d = bonafide.do_get_smtp_cert()
+        d.addCallback(save_cert)
+        return d
+
+
+class KeysCmd(SubCommand):
+
+    label = 'keys'
+
+    @register_method("[dict]")
+    def do_LIST(self, service, *parts, **kw):
+        private = False
+        if parts[-1] == 'private':
+            private = True
+
+        bonafide = kw['bonafide']
+        d = bonafide.do_get_active_user()
+        d.addCallback(service.do_list_keys, private)
+        return d
+
+    @register_method('dict')
+    def do_EXPORT(self, service, *parts, **kw):
+        if len(parts) < 3:
+            return defer.fail("An email address is needed")
+        address = parts[2]
+
+        private = False
+        if parts[-1] == 'private':
+            private = True
+
+        bonafide = kw['bonafide']
+        d = bonafide.do_get_active_user()
+        d.addCallback(service.do_export, address, private)
+        return d
+
+    @register_method('dict')
+    def do_INSERT(self, service, *parts, **kw):
+        if len(parts) < 5:
+            return defer.fail("An email address is needed")
+        address = parts[2]
+        validation = parts[3]
+        rawkey = parts[4]
+
+        bonafide = kw['bonafide']
+        d = bonafide.do_get_active_user()
+        d.addCallback(service.do_insert, address, rawkey, validation)
+        return d
+
+    @register_method('str')
+    def do_DELETE(self, service, *parts, **kw):
+        if len(parts) < 3:
+            return defer.fail("An email address is needed")
+        address = parts[2]
+
+        private = False
+        if parts[-1] == 'private':
+            private = True
+
+        bonafide = kw['bonafide']
+        d = bonafide.do_get_active_user()
+        d.addCallback(service.do_delete, address, private)
+        return d
+
+
+class CommandDispatcher(object):
+
+    __metaclass__ = APICommand
+
+    label = 'core'
+
+    def __init__(self, core):
+
+        self.core = core
+        self.subcommand_user = UserCmd()
+        self.subcommand_eip = EIPCmd()
+        self.subcommand_mail = MailCmd()
+        self.subcommand_keys = KeysCmd()
+
+    # XXX --------------------------------------------
+    # TODO move general services to another subclass
+
+    @register_method("{'mem_usage': str}")
+    def do_STATS(self, *parts):
+        return _format_result(self.core.do_stats())
+
+    @register_method("{version_core': '0.0.0'}")
+    def do_VERSION(self, *parts):
+        return _format_result(self.core.do_version())
+
+    @register_method("{'mail': 'running'}")
+    def do_STATUS(self, *parts):
+        return _format_result(self.core.do_status())
+
+    @register_method("{'shutdown': 'ok'}")
+    def do_SHUTDOWN(self, *parts):
+        return _format_result(self.core.do_shutdown())
+
+    # -----------------------------------------------
+
+    def do_USER(self, *parts):
+        bonafide = self._get_service('bonafide')
+        d = self.subcommand_user.dispatch(bonafide, *parts)
+        d.addCallbacks(_format_result, _format_error)
+        return d
+
+    def do_EIP(self, *parts):
+        eip = self._get_service(self.subcommand_eip.label)
+        if not eip:
+            return _format_result('eip: disabled')
+        subcmd = parts[1]
+
+        dispatch = self._subcommand_eip.dispatch
+        if subcmd in ('enable', 'disable'):
+            d = dispatch(self.core, *parts)
+        else:
+            d = dispatch(eip, *parts)
+
+        d.addCallbacks(_format_result, _format_error)
+        return d
+
+    def do_MAIL(self, *parts):
+        subcmd = parts[1]
+        dispatch = self.subcommand_mail.dispatch
+
+        if subcmd == 'enable':
+            d = dispatch(self.core, *parts)
+
+        mail = self._get_service(self.subcommand_mail.label)
+        bonafide = self._get_service('bonafide')
+        kw = {'bonafide': bonafide}
+
+        if not mail:
+            return _format_result('mail: disabled')
+
+        if subcmd == 'disable':
+            d = dispatch(self.core)
+        else:
+            d = dispatch(mail, *parts, **kw)
+
+        d.addCallbacks(_format_result, _format_error)
+        return d
+
+    def do_KEYS(self, *parts):
+        dispatch = self.subcommand_keys.dispatch
+
+        keymanager_label = 'keymanager'
+        keymanager = self._get_service(keymanager_label)
+        bonafide = self._get_service('bonafide')
+        kw = {'bonafide': bonafide}
+
+        if not keymanager:
+            return _format_result('keymanager: disabled')
+
+        d = dispatch(keymanager, *parts, **kw)
+        d.addCallbacks(_format_result, _format_error)
+        return d
+
+    def dispatch(self, msg):
+        cmd = msg[0]
+
+        _method = getattr(self, 'do_' + cmd.upper(), None)
+
+        if not _method:
+            return defer.fail(failure.Failure(RuntimeError('No such command')))
+
+        return defer.maybeDeferred(_method, *msg)
+
+    def _get_service(self, name):
+        try:
+            return self.core.getServiceNamed(name)
+        except KeyError:
+            return None
+
+
+def _format_result(result):
+    return json.dumps({'error': None, 'result': result})
+
+
+def _format_error(failure):
+    log.err(failure)
+    return json.dumps({'error': failure.value.message, 'result': None})
diff --git a/src/leap/bitmask/core/dummy.py b/src/leap/bitmask/core/dummy.py
new file mode 100644
index 00000000..7b6be397
--- /dev/null
+++ b/src/leap/bitmask/core/dummy.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+# dummy.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 <http://www.gnu.org/licenses/>.
+"""
+An authoritative dummy backend for tests.
+"""
+import json
+
+from leap.common.service_hooks import HookableService
+
+
+class BackendCommands(object):
+
+    """
+    General commands for the BitmaskBackend Core Service.
+    """
+
+    def __init__(self, core):
+        self.core = core
+
+    def do_status(self):
+        return json.dumps(
+            {'soledad': 'running',
+             'keymanager': 'running',
+             'mail': 'running',
+             'eip': 'stopped',
+             'backend': 'dummy'})
+
+    def do_version(self):
+        return {'version_core': '0.0.1'}
+
+    def do_stats(self):
+        return {'mem_usage': '01 KB'}
+
+    def do_shutdown(self):
+        return {'shutdown': 'ok'}
+
+
+class mail_services(object):
+
+    class SoledadService(HookableService):
+        pass
+
+    class KeymanagerService(HookableService):
+        pass
+
+    class StandardMailService(HookableService):
+        pass
+
+
+class BonafideService(HookableService):
+
+    def __init__(self, basedir):
+        pass
+
+    def do_authenticate(self, user, password):
+        return {u'srp_token': u'deadbeef123456789012345678901234567890123',
+                u'uuid': u'01234567890abcde01234567890abcde'}
+
+    def do_signup(self, user, password):
+        return {'signup': 'ok', 'user': 'dummyuser@provider.example.org'}
+
+    def do_logout(self, user):
+        return {'logout': 'ok'}
+
+    def do_get_active_user(self):
+        return 'dummyuser@provider.example.org'
diff --git a/src/leap/bitmask/core/flags.py b/src/leap/bitmask/core/flags.py
new file mode 100644
index 00000000..9a40c70c
--- /dev/null
+++ b/src/leap/bitmask/core/flags.py
@@ -0,0 +1 @@
+BACKEND = 'default'
diff --git a/src/leap/bitmask/core/launcher.py b/src/leap/bitmask/core/launcher.py
new file mode 100644
index 00000000..b8916a1e
--- /dev/null
+++ b/src/leap/bitmask/core/launcher.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# launcher.py
+# Copyright (C) 2016 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+Run bitmask daemon.
+"""
+from os.path import join
+from sys import argv
+
+from twisted.scripts.twistd import run
+
+from leap.bitmask.util import here
+from leap.bitmask import core
+from leap.bitmask.core import flags
+
+
+def run_bitmaskd():
+    # TODO --- configure where to put the logs... (get --logfile, --logdir
+    # from the bitmask_cli
+    for (index, arg) in enumerate(argv):
+        if arg == '--backend':
+            flags.BACKEND = argv[index + 1]
+    argv[1:] = [
+        '-y', join(here(core), "bitmaskd.tac"),
+        '--pidfile', '/tmp/bitmaskd.pid',
+        '--logfile', '/tmp/bitmaskd.log',
+        '--umask=0022',
+    ]
+    print '[+] launching bitmaskd...'
+    run()
+
+
+if __name__ == "__main__":
+    run_bitmaskd()
diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py
new file mode 100644
index 00000000..68cb444a
--- /dev/null
+++ b/src/leap/bitmask/core/mail_services.py
@@ -0,0 +1,690 @@
+# -*- coding: utf-8 -*-
+# mail_services.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 <http://www.gnu.org/licenses/>.
+"""
+Mail services.
+
+This is quite moving work still.
+This should be moved to the different packages when it stabilizes.
+"""
+import json
+import os
+from collections import defaultdict
+from collections import namedtuple
+
+from twisted.application import service
+from twisted.internet import defer
+from twisted.python import log
+
+from leap.bonafide import config
+from leap.common.service_hooks import HookableService
+from leap.keymanager import KeyManager
+from leap.keymanager.errors import KeyNotFound
+from leap.keymanager.validation import ValidationLevels
+from leap.soledad.client.api import Soledad
+from leap.mail.constants import INBOX_NAME
+from leap.mail.mail import Account
+from leap.mail.imap.service import imap
+from leap.mail.incoming.service import IncomingMail, INCOMING_CHECK_PERIOD
+from leap.mail import smtp
+
+from leap.bitmask.core.uuid_map import UserMap
+from leap.bitmask.core.configurable import DEFAULT_BASEDIR
+
+
+class Container(object):
+
+    def __init__(self, service=None):
+        self._instances = defaultdict(None)
+        if service is not None:
+            self.service = service
+
+    def get_instance(self, key):
+        return self._instances.get(key, None)
+
+    def add_instance(self, key, data):
+        self._instances[key] = data
+
+
+class ImproperlyConfigured(Exception):
+    pass
+
+
+class SoledadContainer(Container):
+
+    def __init__(self, service=None, basedir=DEFAULT_BASEDIR):
+        self._basedir = os.path.expanduser(basedir)
+        self._usermap = UserMap()
+        super(SoledadContainer, self).__init__(service=service)
+
+    def add_instance(self, userid, passphrase, uuid=None, token=None):
+
+        if not uuid:
+            bootstrapped_uuid = self._usermap.lookup_uuid(userid, passphrase)
+            uuid = bootstrapped_uuid
+            if not uuid:
+                return
+        else:
+            self._usermap.add(userid, uuid, passphrase)
+
+        user, provider = userid.split('@')
+
+        soledad_path = os.path.join(self._basedir, 'soledad')
+        soledad_url = _get_soledad_uri(self._basedir, provider)
+        cert_path = _get_ca_cert_path(self._basedir, provider)
+
+        soledad = self._create_soledad_instance(
+            uuid, passphrase, soledad_path, soledad_url,
+            cert_path, token)
+
+        super(SoledadContainer, self).add_instance(userid, soledad)
+
+        data = {'user': userid, 'uuid': uuid, 'token': token,
+                'soledad': soledad}
+        self.service.trigger_hook('on_new_soledad_instance', **data)
+
+    def _create_soledad_instance(self, uuid, passphrase, soledad_path,
+                                 server_url, cert_file, token):
+        # setup soledad info
+        secrets_path = os.path.join(soledad_path, '%s.secret' % uuid)
+        local_db_path = os.path.join(soledad_path, '%s.db' % uuid)
+
+        if token is None:
+            syncable = False
+            token = ''
+        else:
+            syncable = True
+
+        return Soledad(
+            uuid,
+            unicode(passphrase),
+            secrets_path=secrets_path,
+            local_db_path=local_db_path,
+            server_url=server_url,
+            cert_file=cert_file,
+            auth_token=token,
+            defer_encryption=True,
+            syncable=syncable)
+
+    def set_remote_auth_token(self, userid, token):
+        self.get_instance(userid).token = token
+
+    def set_syncable(self, userid, state):
+        # TODO should check that there's a token!
+        self.get_instance(userid).set_syncable(bool(state))
+
+    def sync(self, userid):
+        self.get_instance(userid).sync()
+
+
+def _get_provider_from_full_userid(userid):
+    _, provider_id = config.get_username_and_provider(userid)
+    return config.Provider(provider_id)
+
+
+def is_service_ready(service, provider):
+    """
+    Returns True when the following conditions are met:
+       - Provider offers that service.
+       - We have the config files for the service.
+       - The service is enabled.
+    """
+    has_service = provider.offers_service(service)
+    has_config = provider.has_config_for_service(service)
+    is_enabled = provider.is_service_enabled(service)
+    return has_service and has_config and is_enabled
+
+
+class SoledadService(HookableService):
+
+    def __init__(self, basedir):
+        service.Service.__init__(self)
+        self._basedir = basedir
+
+    def startService(self):
+        log.msg('Starting Soledad Service')
+        self._container = SoledadContainer(service=self)
+        super(SoledadService, self).startService()
+
+    # hooks
+
+    def hook_on_passphrase_entry(self, **kw):
+        userid = kw.get('username')
+        provider = _get_provider_from_full_userid(userid)
+        provider.callWhenReady(self._hook_on_passphrase_entry, provider, **kw)
+
+    def _hook_on_passphrase_entry(self, provider, **kw):
+        if is_service_ready('mx', provider):
+            userid = kw.get('username')
+            password = kw.get('password')
+            uuid = kw.get('uuid')
+            container = self._container
+            log.msg("on_passphrase_entry: New Soledad Instance: %s" % userid)
+            if not container.get_instance(userid):
+                container.add_instance(userid, password, uuid=uuid, token=None)
+        else:
+            log.msg('Service MX is not ready...')
+
+    def hook_on_bonafide_auth(self, **kw):
+        userid = kw['username']
+        provider = _get_provider_from_full_userid(userid)
+        provider.callWhenReady(self._hook_on_bonafide_auth, provider, **kw)
+
+    def _hook_on_bonafide_auth(self, provider, **kw):
+        if provider.offers_service('mx'):
+            userid = kw['username']
+            password = kw['password']
+            token = kw['token']
+            uuid = kw['uuid']
+
+            container = self._container
+            if container.get_instance(userid):
+                log.msg("Passing a new SRP Token to Soledad: %s" % userid)
+                container.set_remote_auth_token(userid, token)
+                container.set_syncable(userid, True)
+            else:
+                log.msg("Adding a new Soledad Instance: %s" % userid)
+                container.add_instance(
+                    userid, password, uuid=uuid, token=token)
+
+
+class KeymanagerContainer(Container):
+
+    def __init__(self, service=None, basedir=DEFAULT_BASEDIR):
+        self._basedir = os.path.expanduser(basedir)
+        super(KeymanagerContainer, self).__init__(service=service)
+
+    def add_instance(self, userid, token, uuid, soledad):
+
+        keymanager = self._create_keymanager_instance(
+            userid, token, uuid, soledad)
+
+        d = self._get_or_generate_keys(keymanager, userid)
+        d.addCallback(self._on_keymanager_ready_cb, userid, soledad)
+        return d
+
+    def set_remote_auth_token(self, userid, token):
+        self.get_instance(userid)._token = token
+
+    def _on_keymanager_ready_cb(self, keymanager, userid, soledad):
+        # TODO use onready-deferreds instead
+        super(KeymanagerContainer, self).add_instance(userid, keymanager)
+
+        log.msg("Adding Keymanager instance for: %s" % userid)
+        data = {'userid': userid, 'soledad': soledad, 'keymanager': keymanager}
+        self.service.trigger_hook('on_new_keymanager_instance', **data)
+
+    def _get_or_generate_keys(self, keymanager, userid):
+
+        def if_not_found_generate(failure):
+            # TODO -------------- should ONLY generate if INITIAL_SYNC_DONE.
+            # ie: put callback on_soledad_first_sync_ready -----------------
+            # --------------------------------------------------------------
+            failure.trap(KeyNotFound)
+            log.msg("Core: Key not found. Generating key for %s" % (userid,))
+            d = keymanager.gen_key()
+            d.addCallbacks(send_key, log_key_error("generating"))
+            return d
+
+        def send_key(ignored):
+            # ----------------------------------------------------------------
+            # It might be the case that we have generated a key-pair
+            # but this hasn't been successfully uploaded. How do we know that?
+            # XXX Should this be a method of bonafide instead?
+            # -----------------------------------------------------------------
+            d = keymanager.send_key()
+            d.addCallbacks(
+                lambda _: log.msg(
+                    "Key generated successfully for %s" % userid),
+                log_key_error("sending"))
+            return d
+
+        def log_key_error(step):
+            def log_error(failure):
+                log.err("Error while %s key!" % step)
+                log.err(failure)
+                return failure
+            return log_error
+
+        d = keymanager.get_key(userid, private=True, fetch_remote=False)
+        d.addErrback(if_not_found_generate)
+        d.addCallback(lambda _: keymanager)
+        return d
+
+    def _create_keymanager_instance(self, userid, token, uuid, soledad):
+        user, provider = userid.split('@')
+        nickserver_uri = self._get_nicknym_uri(provider)
+
+        cert_path = _get_ca_cert_path(self._basedir, provider)
+        api_uri = self._get_api_uri(provider)
+
+        if not token:
+            token = self.service.tokens.get(userid)
+
+        km_args = (userid, nickserver_uri, soledad)
+
+        # TODO use the method in
+        # services.soledadbootstrapper._get_gpg_bin_path.
+        # That should probably live in keymanager package.
+
+        km_kwargs = {
+            "token": token, "uid": uuid,
+            "api_uri": api_uri, "api_version": "1",
+            "ca_cert_path": cert_path,
+            "gpgbinary": "/usr/bin/gpg"
+        }
+        keymanager = KeyManager(*km_args, **km_kwargs)
+        return keymanager
+
+    def _get_api_uri(self, provider):
+        # TODO get this from service.json (use bonafide service)
+        api_uri = "https://api.{provider}:4430".format(
+            provider=provider)
+        return api_uri
+
+    def _get_nicknym_uri(self, provider):
+        return 'https://nicknym.{provider}:6425'.format(
+            provider=provider)
+
+
+class KeymanagerService(HookableService):
+
+    def __init__(self, basedir=DEFAULT_BASEDIR):
+        service.Service.__init__(self)
+        self._basedir = basedir
+
+    def startService(self):
+        log.msg('Starting Keymanager Service')
+        self._container = KeymanagerContainer(self._basedir)
+        self._container.service = self
+        self.tokens = {}
+        super(KeymanagerService, self).startService()
+
+    # hooks
+
+    def hook_on_new_soledad_instance(self, **kw):
+        container = self._container
+        user = kw['user']
+        token = kw['token']
+        uuid = kw['uuid']
+        soledad = kw['soledad']
+        if not container.get_instance(user):
+            log.msg('Adding a new Keymanager instance for %s' % user)
+            if not token:
+                token = self.tokens.get(user)
+            container.add_instance(user, token, uuid, soledad)
+
+    def hook_on_bonafide_auth(self, **kw):
+        userid = kw['username']
+        provider = _get_provider_from_full_userid(userid)
+        provider.callWhenReady(self._hook_on_bonafide_auth, provider, **kw)
+
+    def _hook_on_bonafide_auth(self, provider, **kw):
+        if provider.offers_service('mx'):
+            userid = kw['username']
+            token = kw['token']
+
+            container = self._container
+            if container.get_instance(userid):
+                log.msg('Passing a new SRP Token to Keymanager: %s' % userid)
+                container.set_remote_auth_token(userid, token)
+            else:
+                log.msg('storing the keymanager token... %s ' % token)
+                self.tokens[userid] = token
+
+    # commands
+
+    def do_list_keys(self, userid, private=False):
+        km = self._container.get_instance(userid)
+        d = km.get_all_keys(private=private)
+        d.addCallback(lambda keys: [dict(key) for key in keys])
+        return d
+
+    def do_export(self, userid, address, private=False):
+        km = self._container.get_instance(userid)
+        d = km.get_key(address, private=private, fetch_remote=False)
+        d.addCallback(lambda key: dict(key))
+        return d
+
+    def do_insert(self, userid, address, rawkey, validation='Fingerprint'):
+        km = self._container.get_instance(userid)
+        validation = ValidationLevels.get(validation)
+        d = km.put_raw_key(rawkey, address, validation=validation)
+        d.addCallback(lambda _: km.get_key(address, fetch_remote=False))
+        d.addCallback(lambda key: dict(key))
+        return d
+
+    @defer.inlineCallbacks
+    def do_delete(self, userid, address, private=False):
+        km = self._container.get_instance(userid)
+        key = yield km.get_key(address, private=private, fetch_remote=False)
+        km.delete_key(key)
+        defer.returnValue(key.fingerprint)
+
+
+class StandardMailService(service.MultiService, HookableService):
+    """
+    A collection of Services.
+
+    This is the parent service, that launches 3 different services that expose
+    Encrypted Mail Capabilities on specific ports:
+
+        - SMTP service, on port 2013
+        - IMAP service, on port 1984
+        - The IncomingMail Service, which doesn't listen on any port, but
+          watches and processes the Incoming Queue and saves the processed mail
+          into the matching INBOX.
+    """
+
+    name = 'mail'
+
+    # TODO factor out Mail Service to inside mail package.
+
+    subscribed_to_hooks = ('on_new_keymanager_instance',)
+
+    def __init__(self, basedir):
+        self._basedir = basedir
+        self._soledad_sessions = {}
+        self._keymanager_sessions = {}
+        self._sendmail_opts = {}
+        self._service_tokens = {}
+        self._active_user = None
+        super(StandardMailService, self).__init__()
+        self.initializeChildrenServices()
+
+    def initializeChildrenServices(self):
+        self.addService(IMAPService(self._soledad_sessions))
+        self.addService(SMTPService(
+            self._soledad_sessions, self._keymanager_sessions,
+            self._sendmail_opts))
+        # TODO adapt the service to receive soledad/keymanager sessions object.
+        # See also the TODO before IncomingMailService.startInstance
+        self.addService(IncomingMailService(self))
+
+    def startService(self):
+        log.msg('Starting Mail Service...')
+        super(StandardMailService, self).startService()
+
+    def stopService(self):
+        super(StandardMailService, self).stopService()
+
+    def startInstance(self, userid, soledad, keymanager):
+        username, provider = userid.split('@')
+
+        self._soledad_sessions[userid] = soledad
+        self._keymanager_sessions[userid] = keymanager
+
+        sendmail_opts = _get_sendmail_opts(self._basedir, provider, username)
+        self._sendmail_opts[userid] = sendmail_opts
+
+        incoming = self.getServiceNamed('incoming_mail')
+        incoming.startInstance(userid)
+
+        def registerToken(token):
+            self._service_tokens[userid] = token
+            self._active_user = userid
+
+        d = soledad.get_or_create_service_token('mail_auth')
+        d.addCallback(registerToken)
+        return d
+
+    def stopInstance(self):
+        pass
+
+    # hooks
+
+    def hook_on_new_keymanager_instance(self, **kw):
+        # XXX we can specify this as a waterfall, or just AND the two
+        # conditions.
+        userid = kw['userid']
+        soledad = kw['soledad']
+        keymanager = kw['keymanager']
+
+        # TODO --- only start instance if "autostart" is True.
+        self.startInstance(userid, soledad, keymanager)
+
+    # commands
+
+    def do_status(self):
+        status = 'running' if self.running else 'disabled'
+        return {'mail': status}
+
+    def get_token(self):
+        active_user = self._active_user
+        if not active_user:
+            return defer.succeed({'user': None})
+        token = self._service_tokens.get(active_user)
+        return defer.succeed({'user': active_user, 'token': token})
+
+    def do_get_smtp_cert_path(self, userid):
+        username, provider = userid.split('@')
+        return _get_smtp_client_cert_path(self._basedir, provider, username)
+
+    # access to containers
+
+    def get_soledad_session(self, userid):
+        return self._soledad_sessions.get(userid)
+
+    def get_keymanager_session(self, userid):
+        return self._keymanager_sessions.get(userid)
+
+
+class IMAPService(service.Service):
+
+    name = 'imap'
+
+    def __init__(self, soledad_sessions):
+        port, factory = imap.run_service(soledad_sessions)
+
+        self._port = port
+        self._factory = factory
+        self._soledad_sessions = soledad_sessions
+        super(IMAPService, self).__init__()
+
+    def startService(self):
+        log.msg('Starting IMAP Service')
+        super(IMAPService, self).startService()
+
+    def stopService(self):
+        self._port.stopListening()
+        self._factory.doStop()
+        super(IMAPService, self).stopService()
+
+
+class SMTPService(service.Service):
+
+    name = 'smtp'
+
+    def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts,
+                 basedir=DEFAULT_BASEDIR):
+
+        self._basedir = os.path.expanduser(basedir)
+        port, factory = smtp.run_service(
+            soledad_sessions, keymanager_sessions, sendmail_opts)
+        self._port = port
+        self._factory = factory
+        self._soledad_sessions = soledad_sessions
+        self._keymanager_sessions = keymanager_sessions
+        self._sendmail_opts = sendmail_opts
+        super(SMTPService, self).__init__()
+
+    def startService(self):
+        log.msg('Starting SMTP Service')
+        super(SMTPService, self).startService()
+
+    def stopService(self):
+        # TODO cleanup all instances
+        super(SMTPService, self).stopService()
+
+
+class IncomingMailService(service.Service):
+
+    name = 'incoming_mail'
+
+    def __init__(self, mail_service):
+        super(IncomingMailService, self).__init__()
+        self._mail = mail_service
+        self._instances = {}
+
+    def startService(self):
+        log.msg('Starting IncomingMail Service')
+        super(IncomingMailService, self).startService()
+
+    def stopService(self):
+        super(IncomingMailService, self).stopService()
+
+    # Individual accounts
+
+    # TODO IncomingMail *IS* already a service.
+    # I think we should better model the current Service
+    # as a startInstance inside a container, and get this
+    # multi-tenant service inside the leap.mail.incoming.service.
+    # ... or just simply make it a multiService and set per-user
+    # instances as Child of this parent.
+
+    def startInstance(self, userid):
+        soledad = self._mail.get_soledad_session(userid)
+        keymanager = self._mail.get_keymanager_session(userid)
+
+        log.msg('Starting Incoming Mail instance for %s' % userid)
+        self._start_incoming_mail_instance(
+            keymanager, soledad, userid)
+
+    def stopInstance(self, userid):
+        # TODO toggle offline!
+        pass
+
+    def _start_incoming_mail_instance(self, keymanager, soledad,
+                                      userid, start_sync=True):
+
+        def setUpIncomingMail(inbox):
+            incoming_mail = IncomingMail(
+                keymanager, soledad,
+                inbox, userid,
+                check_period=INCOMING_CHECK_PERIOD)
+            return incoming_mail
+
+        def registerInstance(incoming_instance):
+            self._instances[userid] = incoming_instance
+            if start_sync:
+                incoming_instance.startService()
+
+        acc = Account(soledad, userid)
+        d = acc.callWhenReady(
+            lambda _: acc.get_collection_by_mailbox(INBOX_NAME))
+        d.addCallback(setUpIncomingMail)
+        d.addCallback(registerInstance)
+        d.addErrback(log.err)
+        return d
+
+# --------------------------------------------------------------------
+#
+# config utilities. should be moved to bonafide
+#
+
+SERVICES = ('soledad', 'smtp', 'eip')
+
+
+Provider = namedtuple(
+    'Provider', ['hostname', 'ip_address', 'location', 'port'])
+
+SendmailOpts = namedtuple(
+    'SendmailOpts', ['cert', 'key', 'hostname', 'port'])
+
+
+def _get_ca_cert_path(basedir, provider):
+    path = os.path.join(
+        basedir, 'providers', provider, 'keys', 'ca', 'cacert.pem')
+    return path
+
+
+def _get_sendmail_opts(basedir, provider, username):
+    cert = _get_smtp_client_cert_path(basedir, provider, username)
+    key = cert
+    prov = _get_provider_for_service('smtp', basedir, provider)
+    hostname = prov.hostname
+    port = prov.port
+    opts = SendmailOpts(cert, key, hostname, port)
+    return opts
+
+
+def _get_smtp_client_cert_path(basedir, provider, username):
+    path = os.path.join(
+        basedir, 'providers', provider, 'keys', 'client', 'stmp_%s.pem' %
+        username)
+    return path
+
+
+def _get_config_for_service(service, basedir, provider):
+    if service not in SERVICES:
+        raise ImproperlyConfigured('Tried to use an unknown service')
+
+    config_path = os.path.join(
+        basedir, 'providers', provider, '%s-service.json' % service)
+    try:
+        with open(config_path) as config:
+            config = json.loads(config.read())
+    except IOError:
+        # FIXME might be that the provider DOES NOT offer this service!
+        raise ImproperlyConfigured(
+            'could not open config file %s' % config_path)
+    else:
+        return config
+
+
+def first(xs):
+    return xs[0]
+
+
+def _pick_server(config, strategy=first):
+    """
+    Picks a server from a list of possible choices.
+    The service files have a  <describe>.
+    This implementation just picks the FIRST available server.
+    """
+    servers = config['hosts'].keys()
+    choice = config['hosts'][strategy(servers)]
+    return choice
+
+
+def _get_subdict(d, keys):
+    return {key: d.get(key) for key in keys}
+
+
+def _get_provider_for_service(service, basedir, provider):
+
+    if service not in SERVICES:
+        raise ImproperlyConfigured('Tried to use an unknown service')
+
+    config = _get_config_for_service(service, basedir, provider)
+    p = _pick_server(config)
+    attrs = _get_subdict(p, ('hostname', 'ip_address', 'location', 'port'))
+    provider = Provider(**attrs)
+    return provider
+
+
+def _get_smtp_uri(basedir, provider):
+    prov = _get_provider_for_service('smtp', basedir, provider)
+    url = 'https://{hostname}:{port}'.format(
+        hostname=prov.hostname, port=prov.port)
+    return url
+
+
+def _get_soledad_uri(basedir, provider):
+    prov = _get_provider_for_service('soledad', basedir, provider)
+    url = 'https://{hostname}:{port}'.format(
+        hostname=prov.hostname, port=prov.port)
+    return url
diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py
new file mode 100644
index 00000000..99132c2d
--- /dev/null
+++ b/src/leap/bitmask/core/service.py
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 -*-
+# service.py
+# Copyright (C) 2015 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+Bitmask-core Service.
+"""
+import json
+import resource
+
+from twisted.internet import reactor
+from twisted.python import log
+
+from leap.bitmask import __version__
+from leap.bitmask.core import configurable
+from leap.bitmask.core import _zmq
+from leap.bitmask.core import flags
+from leap.common.events import server as event_server
+# from leap.vpn import EIPService
+
+
+backend = flags.BACKEND
+
+if backend == 'default':
+    from leap.bitmask.core import mail_services
+    from leap.bonafide.service import BonafideService
+elif backend == 'dummy':
+    from leap.bitmask.core.dummy import mail_services
+    from leap.bitmask.core.dummy import BonafideService
+else:
+    raise RuntimeError('Backend not supported')
+
+
+class BitmaskBackend(configurable.ConfigurableService):
+
+    def __init__(self, basedir=configurable.DEFAULT_BASEDIR):
+
+        configurable.ConfigurableService.__init__(self, basedir)
+        self.core_commands = BackendCommands(self)
+
+        def enabled(service):
+            return self.get_config('services', service, False, boolean=True)
+
+        on_start = reactor.callWhenRunning
+
+        on_start(self.init_events)
+        on_start(self.init_bonafide)
+
+        if enabled('mail'):
+            on_start(self.init_soledad)
+            on_start(self.init_keymanager)
+            on_start(self.init_mail)
+
+        if enabled('eip'):
+            on_start(self.init_eip)
+
+        if enabled('zmq'):
+            on_start(self.init_zmq)
+
+        if enabled('web'):
+            on_start(self.init_web)
+
+    def init_events(self):
+        event_server.ensure_server()
+
+    def init_bonafide(self):
+        bf = BonafideService(self.basedir)
+        bf.setName("bonafide")
+        bf.setServiceParent(self)
+        # TODO ---- these hooks should be activated only if
+        # (1) we have enabled that service
+        # (2) provider offers this service
+        bf.register_hook('on_passphrase_entry', listener='soledad')
+        bf.register_hook('on_bonafide_auth', listener='soledad')
+        bf.register_hook('on_bonafide_auth', listener='keymanager')
+
+    def init_soledad(self):
+        service = mail_services.SoledadService
+        sol = self._maybe_start_service(
+            'soledad', service, self.basedir)
+        if sol:
+            sol.register_hook(
+                'on_new_soledad_instance', listener='keymanager')
+
+    def init_keymanager(self):
+        service = mail_services.KeymanagerService
+        km = self._maybe_start_service(
+            'keymanager', service, self.basedir)
+        if km:
+            km.register_hook('on_new_keymanager_instance', listener='mail')
+
+    def init_mail(self):
+        service = mail_services.StandardMailService
+        self._maybe_start_service('mail', service, self.basedir)
+
+    def init_eip(self):
+        # FIXME -- land EIP into leap.vpn
+        pass
+        # self._maybe_start_service('eip', EIPService)
+
+    def init_zmq(self):
+        zs = _zmq.ZMQServerService(self)
+        zs.setServiceParent(self)
+
+    def init_web(self):
+        from leap.bitmask.core import websocket
+        ws = websocket.WebSocketsDispatcherService(self)
+        ws.setServiceParent(self)
+
+    def _maybe_start_service(self, label, klass, *args, **kw):
+        try:
+            self.getServiceNamed(label)
+        except KeyError:
+            service = klass(*args, **kw)
+            service.setName(label)
+            service.setServiceParent(self)
+            return service
+
+    def do_stats(self):
+        return self.core_commands.do_stats()
+
+    def do_status(self):
+        return self.core_commands.do_status()
+
+    def do_version(self):
+        return self.core_commands.do_version()
+
+    def do_shutdown(self):
+        return self.core_commands.do_shutdown()
+
+    # Service Toggling
+
+    def do_enable_service(self, service):
+        assert service in self.service_names
+        self.set_config('services', service, 'True')
+
+        if service == 'mail':
+            self.init_soledad()
+            self.init_keymanager()
+            self.init_mail()
+
+        elif service == 'eip':
+            self.init_eip()
+
+        elif service == 'zmq':
+            self.init_zmq()
+
+        elif service == 'web':
+            self.init_web()
+
+        return 'ok'
+
+    def do_disable_service(self, service):
+        assert service in self.service_names
+        # TODO -- should stop also?
+        self.set_config('services', service, 'False')
+        return 'ok'
+
+
+class BackendCommands(object):
+
+    """
+    General commands for the BitmaskBackend Core Service.
+    """
+
+    def __init__(self, core):
+        self.core = core
+
+    def do_status(self):
+        # we may want to make this tuple a class member
+        services = ('soledad', 'keymanager', 'mail', 'eip')
+
+        status = {}
+        for name in services:
+            _status = 'stopped'
+            try:
+                if self.core.getServiceNamed(name).running:
+                    _status = 'running'
+            except KeyError:
+                pass
+            status[name] = _status
+        status['backend'] = flags.BACKEND
+
+        return json.dumps(status)
+
+    def do_version(self):
+        return {'version_core': __version__}
+
+    def do_stats(self):
+        log.msg('BitmaskCore Service STATS')
+        mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
+        return {'mem_usage': '%s KB' % (mem / 1024)}
+
+    def do_shutdown(self):
+        self.core.stopService()
+        reactor.callLater(1, reactor.stop)
+        return {'shutdown': 'ok'}
diff --git a/src/leap/bitmask/core/uuid_map.py b/src/leap/bitmask/core/uuid_map.py
new file mode 100644
index 00000000..5edc7216
--- /dev/null
+++ b/src/leap/bitmask/core/uuid_map.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+# uuid_map.py
+# Copyright (C) 2015,2016 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+UUID Map: a persistent mapping between user-ids and uuids.
+"""
+
+import base64
+import os
+import re
+
+import scrypt
+
+from leap.common.config import get_path_prefix
+
+
+MAP_PATH = os.path.join(get_path_prefix(), 'leap', 'uuids')
+
+
+class UserMap(object):
+
+    """
+    A persistent mapping between user-ids and uuids.
+    """
+
+    # TODO Add padding to the encrypted string
+
+    def __init__(self):
+        self._d = {}
+        self._lines = set([])
+        if os.path.isfile(MAP_PATH):
+            self.load()
+
+    def add(self, userid, uuid, passwd):
+        """
+        Add a new userid-uuid mapping, and encrypt the record with the user
+        password.
+        """
+        self._add_to_cache(userid, uuid)
+        self._lines.add(_encode_uuid_map(userid, uuid, passwd))
+        self.dump()
+
+    def _add_to_cache(self, userid, uuid):
+        self._d[userid] = uuid
+
+    def load(self):
+        """
+        Load a mapping from a default file.
+        """
+        with open(MAP_PATH, 'r') as infile:
+            lines = infile.readlines()
+            self._lines = set(lines)
+
+    def dump(self):
+        """
+        Dump the mapping to a default file.
+        """
+        with open(MAP_PATH, 'w') as out:
+            out.write('\n'.join(self._lines))
+
+    def lookup_uuid(self, userid, passwd=None):
+        """
+        Lookup the uuid for a given userid.
+
+        If no password is given, try to lookup on cache.
+        Else, try to decrypt all the records that we know about with the
+        passed password.
+        """
+        if not passwd:
+            return self._d.get(userid)
+
+        for line in self._lines:
+            guess = _decode_uuid_line(line, passwd)
+            if guess:
+                record_userid, uuid = guess
+                if record_userid == userid:
+                    self._add_to_cache(userid, uuid)
+                    return uuid
+
+    def lookup_userid(self, uuid):
+        """
+        Get the userid for the given uuid from cache.
+        """
+        rev_d = {v: k for (k, v) in self._d.items()}
+        return rev_d.get(uuid)
+
+
+def _encode_uuid_map(userid, uuid, passwd):
+    data = 'userid:%s:uuid:%s' % (userid, uuid)
+    encrypted = scrypt.encrypt(data, passwd, maxtime=0.05)
+    return base64.encodestring(encrypted).replace('\n', '')
+
+
+def _decode_uuid_line(line, passwd):
+    decoded = base64.decodestring(line)
+    try:
+        maybe_decrypted = scrypt.decrypt(decoded, passwd, maxtime=0.1)
+    except scrypt.error:
+        return None
+    match = re.findall("userid\:(.+)\:uuid\:(.+)", maybe_decrypted)
+    if match:
+        return match[0]
diff --git a/src/leap/bitmask/core/web/__init__.py b/src/leap/bitmask/core/web/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/leap/bitmask/core/web/index.html b/src/leap/bitmask/core/web/index.html
new file mode 100644
index 00000000..9490eca8
--- /dev/null
+++ b/src/leap/bitmask/core/web/index.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+   <head>
+      <title>Bitmask WebSockets Endpoint</title>
+      <script type="text/javascript">
+         var sock = null;
+         var ellog = null;
+
+         window.onload = function() {
+
+            ellog = document.getElementById('log');
+
+            var wsuri;
+            if (window.location.protocol === "file:") {
+               wsuri = "ws://127.0.0.1:8080/ws";
+            } else {
+               wsuri = "ws://" + window.location.hostname + ":8080/bitmask";
+            }
+
+            if ("WebSocket" in window) {
+               sock = new WebSocket(wsuri);
+            } else if ("MozWebSocket" in window) {
+               sock = new MozWebSocket(wsuri);
+            } else {
+               log("Browser does not support WebSocket!");
+               window.location = "http://autobahn.ws/unsupportedbrowser";
+            }
+
+            if (sock) {
+               sock.onopen = function() {
+                  log("Connected to " + wsuri);
+               }
+
+               sock.onclose = function(e) {
+                  log("Connection closed (wasClean = " + e.wasClean + ", code = " + e.code + ", reason = '" + e.reason + "')");
+                  sock = null;
+               }
+
+               sock.onmessage = function(e) {
+                  log("[res] " + e.data + '\n');
+               }
+            }
+         };
+
+         function send() {
+            var msg = document.getElementById('message').value;
+            if (sock) {
+               sock.send(msg);
+               log("[cmd] " + msg);
+            } else {
+               log("Not connected.");
+            }
+         };
+
+         function log(m) {
+            ellog.innerHTML += m + '\n';
+            ellog.scrollTop = ellog.scrollHeight;
+         };
+      </script>
+   </head>
+   <body>
+      <h1>Bitmask Control Panel</h1>
+      <noscript>You must enable JavaScript</noscript>
+      <form>
+         <p>Command: <input id="message" type="text" size="50" maxlength="50" value="status"></p>
+      </form>
+      <button onclick='send();'>Send Command</button>
+      <pre id="log" style="height: 20em; overflow-y: scroll; background-color: #faa;"></pre>
+   </body>
+</html>
diff --git a/src/leap/bitmask/core/web/root.py b/src/leap/bitmask/core/web/root.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/leap/bitmask/core/websocket.py b/src/leap/bitmask/core/websocket.py
new file mode 100644
index 00000000..5569c6c7
--- /dev/null
+++ b/src/leap/bitmask/core/websocket.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# websocket.py
+# Copyright (C) 2015, 2016 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+WebSockets Dispatcher Service.
+"""
+
+import os
+import pkg_resources
+
+from twisted.internet import reactor
+from twisted.application import service
+
+from twisted.web.server import Site
+from twisted.web.static import File
+
+from autobahn.twisted.resource import WebSocketResource
+from autobahn.twisted.websocket import WebSocketServerFactory
+from autobahn.twisted.websocket import WebSocketServerProtocol
+
+from leap.bitmask.core.dispatcher import CommandDispatcher
+
+
+class WebSocketsDispatcherService(service.Service):
+
+    """
+    A Dispatcher for BitmaskCore exposing a WebSockets Endpoint.
+    """
+
+    def __init__(self, core, port=8080, debug=False):
+        self._core = core
+        self.port = port
+        self.debug = debug
+
+    def startService(self):
+
+        factory = WebSocketServerFactory(u"ws://127.0.0.1:%d" % self.port,
+                                         debug=self.debug)
+        factory.protocol = DispatcherProtocol
+        factory.protocol.dispatcher = CommandDispatcher(self._core)
+
+        # FIXME: Site.start/stopFactory should start/stop factories wrapped as
+        # Resources
+        factory.startFactory()
+
+        resource = WebSocketResource(factory)
+
+        # we server static files under "/" ..
+        webdir = os.path.abspath(
+            pkg_resources.resource_filename("leap.bitmask.core", "web"))
+        root = File(webdir)
+
+        # and our WebSocket server under "/ws"
+        root.putChild(u"bitmask", resource)
+
+        # both under one Twisted Web Site
+        site = Site(root)
+
+        self.site = site
+        self.factory = factory
+
+        self.listener = reactor.listenTCP(self.port, site)
+
+    def stopService(self):
+        self.factory.stopFactory()
+        self.site.stopFactory()
+        self.listener.stopListening()
+
+
+class DispatcherProtocol(WebSocketServerProtocol):
+
+    def onMessage(self, msg, binary):
+        parts = msg.split()
+        r = self.dispatcher.dispatch(parts)
+        r.addCallback(self.defer_reply, binary)
+
+    def reply(self, response, binary):
+        self.sendMessage(response, binary)
+
+    def defer_reply(self, response, binary):
+        reactor.callLater(0, self.reply, response, binary)
+
+    def _get_service(self, name):
+        return self.core.getServiceNamed(name)
-- 
cgit v1.2.3