summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordrebs <drebs@leap.se>2013-03-05 17:29:37 -0300
committerdrebs <drebs@leap.se>2013-03-05 17:29:37 -0300
commitbf097914f492a2f3973d0b051324d121353fa5fa (patch)
treedbbcadbe7ba5c0a4e2a931bc6ff56a3698847dcc
parent0baf100c59655d1fbe1424f685328ba2de080c98 (diff)
Add a shared remote database to Soledad (client and server).
-rw-r--r--__init__.py151
-rw-r--r--backends/leap_backend.py32
-rw-r--r--server.py41
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
diff --git a/server.py b/server.py
index 746dc8c0..dbaf6a13 100644
--- a/server.py
+++ b/server.py
@@ -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(','))