diff options
| author | drebs <drebs@leap.se> | 2013-03-05 17:29:37 -0300 | 
|---|---|---|
| committer | drebs <drebs@leap.se> | 2013-03-05 17:29:37 -0300 | 
| commit | bf097914f492a2f3973d0b051324d121353fa5fa (patch) | |
| tree | dbbcadbe7ba5c0a4e2a931bc6ff56a3698847dcc | |
| parent | 0baf100c59655d1fbe1424f685328ba2de080c98 (diff) | |
Add a shared remote database to Soledad (client and server).
| -rw-r--r-- | __init__.py | 151 | ||||
| -rw-r--r-- | backends/leap_backend.py | 32 | ||||
| -rw-r--r-- | server.py | 41 | 
3 files changed, 177 insertions, 47 deletions
| diff --git a/__init__.py b/__init__.py index 54ec783f..78cf27ef 100644 --- a/__init__.py +++ b/__init__.py @@ -14,7 +14,15 @@ import random  import hmac  import configparser  import re -from u1db.remote import http_client +try: +    import simplejson as json +except ImportError: +    import json  # noqa +from u1db import errors +from u1db.remote import ( +    http_client, +    http_database, +)  from leap.soledad.backends import sqlcipher  from leap.soledad.util import GPGWrapper  from leap.soledad.backends.leap_backend import ( @@ -58,12 +66,12 @@ class Soledad(object):          'secret_path': '%s/secret.gpg',          'local_db_path': '%s/soledad.u1db',          'config_file': '%s/soledad.ini', -        'soledad_server_url': '', +        'shared_db_url': '',      }      def __init__(self, user_email, prefix=None, gnupg_home=None,                   secret_path=None, local_db_path=None, -                 config_file=None, soledad_server_url=None, auth_token=None, +                 config_file=None, shared_db_url=None, auth_token=None,                   initialize=True):          """          Bootstrap Soledad, initialize cryptographic material and open @@ -77,13 +85,17 @@ class Soledad(object):               'secret_path': secret_path,               'local_db_path': local_db_path,               'config_file': config_file, -             'soledad_server_url': soledad_server_url, +             'shared_db_url': shared_db_url,               }          ) -        if self.soledad_server_url: -            self._client = SoledadClient(server_url, token=auth_token) +        if self.shared_db_url: +            # TODO: eliminate need to create db here. +            self._shared_db = SoledadSharedDatabase.open_database( +                shared_db_url, +                True, +                token=auth_token)          if initialize: -            self.bootstrap() +            self._bootstrap()      def _bootstrap(self):          """ @@ -113,9 +125,11 @@ class Soledad(object):              except Exception:              # stage 1 bootstrap                  self._init_keys() -                self._send_keys() +                # TODO: change key below +                self._send_keys(self._secret)          # stage 3 bootstrap          self._load_keys() +        self._send_keys(self._secret)          self._init_db()      def _init_config(self, param_conf): @@ -186,7 +200,7 @@ class Soledad(object):      def _has_secret(self):          """ -        Verify if secret for symmetric encryption exists on local encrypted +        Verify if secret for symmetric encryption exists in a local encrypted          file.          """          # does the file exist in disk? @@ -246,11 +260,10 @@ class Soledad(object):          """          Verify if there exists an OpenPGP keypair for this user.          """ -        # TODO: verify if we have the corresponding private key.          try: -            self._gpg.find_key_by_email(self._user_email, secret=True) +            self._load_openpgp_keypair()              return True -        except LookupError: +        except:              return False      def _gen_openpgp_keypair(self): @@ -272,11 +285,15 @@ class Soledad(object):          """          Find fingerprint for this user's OpenPGP keypair.          """ -        if not self._has_openpgp_keypair(): +        # TODO: verify if we have the corresponding private key. +        try: +            self._fingerprint = self._gpg.find_key_by_email( +                self._user_email, +                secret=True)['fingerprint'] +            return self._fingerprint +        except LookupError:              raise KeyDoesNotExist("Tried to load OpenPGP keypair but it does "                                    "not exist on disk.") -        self._fingerprint = self._gpg.find_key_by_email( -            self._user_email)['fingerprint']      def publish_pubkey(self, keyserver):          """ @@ -300,11 +317,26 @@ class Soledad(object):          self._gen_openpgp_keypair()          self._gen_secret() +    def _user_hash(self): +        return hmac.new(self._user_email, 'user').hexdigest() +      def _retrieve_keys(self): -        h = hmac.new(self._user_email, 'user-keys').hexdigest() -        self._client._request_json('GET', ['user-keys', h]) +        return self._shared_db.get_doc_unauth(self._user_hash())          # TODO: create corresponding error on server side +    def _send_keys(self, passphrase): +        privkey = self._gpg.export_keys(self._fingerprint, secret=True) +        content = { +            '_privkey': self.encrypt(privkey, passphrase=passphrase, +                                     symmetric=True), +            '_symkey': self.encrypt(self._secret), +        } +        doc = self._retrieve_keys() +        if not doc: +            doc = LeapDocument(doc_id=self._user_hash(), soledad=self) +        doc.content = content +        self._shared_db.put_doc(doc) +      #-------------------------------------------------------------------------      # Data encryption and decryption      #------------------------------------------------------------------------- @@ -407,7 +439,7 @@ class Soledad(object):  #----------------------------------------------------------------------------- -# Soledad client +# Soledad shared database  #-----------------------------------------------------------------------------  class NoTokenForAuth(Exception): @@ -416,15 +448,34 @@ class NoTokenForAuth(Exception):      """ -class SoledadClient(http_client.HTTPClientBase): +class Unauthorized(Exception): +    """ +    User does not have authorization to perform task. +    """ + + +class SoledadSharedDatabase(http_database.HTTPDatabase): +    """ +    This is a shared HTTP database that holds users' encrypted keys. +    """ +    # TODO: prevent client from messing with the shared DB. +    # TODO: define and document API.      @staticmethod -    def connect(url, token=None): -        return SoledadClient(url, token=token) +    def open_database(url, create, token=None, soledad=None): +        db = SoledadSharedDatabase(url, token=token, soledad=soledad) +        db.open(create) +        return db -    def __init__(self, url, creds=None, token=None): -        super(SoledadClient, self).__init__(url, creds) -        self.token = token +    def delete_database(url): +        raise Unauthorized("Can't delete shared database.") + +    def __init__(self, url, document_factory=None, creds=None, token=None, +                 soledad=None): +        self._set_token(token) +        self._soledad = soledad +        super(SoledadSharedDatabase, self).__init__(url, document_factory, +                                                    creds)      def _set_token(self, token):          self._token = token @@ -435,14 +486,54 @@ class SoledadClient(http_client.HTTPClientBase):      token = property(_get_token, _set_token,                       doc='Token for token-based authentication.') -    def _request_json(self, method, url_parts, params=None, body=None, -                      content_type=None, auth=False): +    def _request(self, method, url_parts, params=None, body=None, +                 content_type=None, auth=True): +        """ +        Perform token-based http request. +        """          if auth: -            if not token: +            if not self.token:                  raise NoTokenForAuth() -            params.update({'auth_token', self.token}) -        super(SoledadClient, self)._request_json(method, url_parts, params, -                                                 body, content_type) +            if not params: +                params = {} +            params['auth_token'] = self.token +        return super(SoledadSharedDatabase, self)._request( +            method, url_parts, +            params, +            body, +            content_type) +    def _request_json(self, method, url_parts, params=None, body=None, +                      content_type=None, auth=True): +        """ +        Perform token-based http request. +        """ +        res, headers = self._request(method, url_parts, +                                     params=params, body=body, +                                     content_type=content_type, auth=auth) +        return json.loads(res), headers + +    def get_doc_unauth(self, doc_id): +        """ +        Modified method to allow for unauth request. +        """ +        try: +            res, headers = self._request( +                'GET', ['doc', doc_id], {"include_deleted": False}, +                auth=False) +        except errors.DocumentDoesNotExist: +            return None +        except errors.HTTPError, e: +            if (e.status == http_database.DOCUMENT_DELETED_STATUS and +                    'x-u1db-rev' in e.headers): +                res = None +                headers = e.headers +            else: +                raise +        doc_rev = headers['x-u1db-rev'] +        has_conflicts = json.loads(headers['x-u1db-has-conflicts']) +        doc = self._factory(doc_id, doc_rev, res) +        doc.has_conflicts = has_conflicts +        return doc  __all__ = ['util'] diff --git a/backends/leap_backend.py b/backends/leap_backend.py index 3d423f5d..a37f9d25 100644 --- a/backends/leap_backend.py +++ b/backends/leap_backend.py @@ -55,25 +55,39 @@ class LeapDocument(Document):          if encrypted_json:              self.set_encrypted_json(encrypted_json) -    def get_encrypted_json(self): +    def get_encrypted_content(self):          """ -        Return document's json serialization encrypted with user's public key. +        Return an encrypted JSON serialization of document's contents.          """          if not self._soledad:              raise NoSoledadInstance() -        ciphertext = self._soledad.encrypt_symmetric(self.doc_id, -                                                     self.get_json()) -        return json.dumps({'_encrypted_json': ciphertext}) +        return self._soledad.encrypt_symmetric(self.doc_id, +                                               self.get_json()) + +    def set_encrypted_content(self, cyphertext): +        """ +        Set document's content based on an encrypted JSON serialization of +        contents. +        """ +        plaintext = self._soledad.decrypt_symmetric(self.doc_id, cyphertext) +        return self.set_json(plaintext) + +    def get_encrypted_json(self): +        """ +        Return a valid JSON string containing document's content encrypted to +        the user's public key. +        """ +        return json.dumps({'_encrypted_json': self.get_encrypted_content()})      def set_encrypted_json(self, encrypted_json):          """ -        Set document's content based on encrypted version of json string. +        Set document's content based on a valid JSON string containing the +        encrypted document's contents.          """          if not self._soledad:              raise NoSoledadInstance() -        ciphertext = json.loads(encrypted_json)['_encrypted_json'] -        plaintext = self._soledad.decrypt_symmetric(self.doc_id, ciphertext) -        return self.set_json(plaintext) +        cyphertext = json.loads(encrypted_json)['_encrypted_json'] +        self.set_encrypted_content(cyphertext)      def _get_syncable(self):          return self._syncable @@ -12,6 +12,7 @@ try:      import simplejson as json  except ImportError:      import json  # noqa +from urlparse import parse_qs  from twisted.web.wsgi import WSGIResource  from twisted.internet import reactor @@ -55,20 +56,28 @@ class SoledadAuthMiddleware(object):      def __call__(self, environ, start_response):          if self.prefix and not environ['PATH_INFO'].startswith(self.prefix):              return self._error(start_response, 400, "bad request") -        token = environ.get('HTTP_AUTHORIZATION') -        if not token: +        shift_path_info(environ) +        qs = parse_qs(environ.get('QUERY_STRING'), strict_parsing=True) +        if 'auth_token' not in qs:              if self.need_auth(environ):                  return self._error(start_response, 401, "unauthorized",                                     "Missing Authentication Token.")          else: +            token = qs['auth_token'][0]              try:                  self.verify_token(environ, token)              except Unauthorized:                  return self._error(                      start_response, 401, "unauthorized",                      "Incorrect password or login.") -            del environ['HTTP_AUTHORIZATION'] -        shift_path_info(environ) +            # remove auth token from query string. +            del qs['auth_token'] +            qs_str = '' +            if qs: +                qs_str = reduce(lambda x, y: '&'.join([x, y]), +                                map(lambda (x, y): '='.join([x, str(y)]), +                                    qs.iteritems())) +            environ['QUERY_STRING'] = qs_str          return self.app(environ, start_response)      def verify_token(self, environ, token): @@ -76,14 +85,29 @@ class SoledadAuthMiddleware(object):          Verify if token is valid for authenticating this action.          """          # TODO: implement token verification -        raise NotImplementedError(self.verify_user) +        raise NotImplementedError(self.verify_token)      def need_auth(self, environ):          """          Check if action can be performed on database without authentication. + +        For now, just allow access to /shared/*.          """ -        # TODO: implement unauth verification. -        raise NotImplementedError(self.allow_unauth) +        # TODO: design unauth verification. +        return not environ.get('PATH_INFO').startswith('/shared/') + + +#----------------------------------------------------------------------------- +# Soledad WSGI application +#----------------------------------------------------------------------------- + +class SoledadApp(http_app.HTTPApp): +    """ +    Soledad WSGI application +    """ + +    def __call__(self, environ, start_response): +        return super(SoledadApp, self).__call__(environ, start_response)  #----------------------------------------------------------------------------- @@ -111,13 +135,14 @@ def load_configuration(file_path):  # Run as Twisted WSGI Resource  #----------------------------------------------------------------------------- +# TODO: create command-line option for choosing config file.  conf = load_configuration('/etc/leap/soledad-server.ini')  state = CouchServerState(conf['couch_url'])  # TODO: change working dir to something meaningful (maybe eliminate it)  state.set_workingdir(conf['working_dir'])  application = SoledadAuthMiddleware( -    http_app.HTTPApp(state), +    SoledadApp(state),      conf['prefix'],      conf['public_dbs'].split(',')) | 
