diff options
-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(',')) |