summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--__init__.py39
-rw-r--r--backends/__init__.py4
-rw-r--r--backends/couch.py31
-rw-r--r--backends/leap_backend.py14
-rw-r--r--backends/objectstore.py13
-rw-r--r--backends/sqlcipher.py3
-rw-r--r--server.py3
-rw-r--r--util.py10
8 files changed, 101 insertions, 16 deletions
diff --git a/__init__.py b/__init__.py
index 4b7a12df..16a7da0c 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,6 +1,12 @@
-# License?
+"""
+Soledad - Synchronization Of Locally Encrypted Data Among Devices.
-"""A U1DB implementation for using Object Stores as its persistence layer."""
+Soledad is the part of LEAP that manages storage and synchronization of
+application data. It is built on top of U1DB reference Python API and
+implements (1) a SQLCipher backend for local storage in the client, (2) a
+SyncTarget that encrypts data to the user's private OpenPGP key before
+syncing, and (3) a CouchDB backend for remote storage in the server side.
+"""
import os
import string
@@ -11,6 +17,13 @@ from leap.soledad.util import GPGWrapper
class Soledad(object):
+ """
+ Soledad client class. It is used to store and fetch data locally in an
+ encrypted manner and request synchronization with Soledad server. This
+ class is also responsible for bootstrapping users' account by creating
+ OpenPGP keys and other cryptographic secrets and/or storing/fetching them
+ on Soledad server.
+ """
# paths
PREFIX = os.environ['HOME'] + '/.config/leap/soledad'
@@ -23,6 +36,10 @@ class Soledad(object):
def __init__(self, user_email, gpghome=None, initialize=True,
prefix=None, secret_path=None, local_db_path=None):
+ """
+ Bootstrap Soledad, initialize cryptographic material and open
+ underlying U1DB database.
+ """
self._user_email = user_email
self.PREFIX = prefix or self.PREFIX
self.SECRET_PATH = secret_path or self.SECRET_PATH
@@ -31,9 +48,13 @@ class Soledad(object):
os.makedirs(self.PREFIX)
self._gpg = GPGWrapper(gpghome=(gpghome or self.GNUPG_HOME))
if initialize:
- self._initialize()
+ self._init_crypto()
+ self._init_db()
- def _initialize(self):
+ def _init_crypto(self):
+ """
+ Load/generate OpenPGP keypair and secret for symmetric encryption.
+ """
# load/generate OpenPGP keypair
if not self._has_openpgp_keypair():
self._gen_openpgp_keypair()
@@ -42,13 +63,19 @@ class Soledad(object):
if not self._has_secret():
self._gen_secret()
self._load_secret()
+
+ def _init_db(self):
# instantiate u1db
- # TODO: verify if secret for sqlcipher should be the same as the one
- # for symmetric encryption.
+ # TODO: verify if secret for sqlcipher should be the same as the
+ # one for symmetric encryption.
self._db = sqlcipher.open(self.LOCAL_DB_PATH, True, self._secret,
soledad=self)
+
def close(self):
+ """
+ Close underlying U1DB database.
+ """
self._db.close()
#-------------------------------------------------------------------------
diff --git a/backends/__init__.py b/backends/__init__.py
index 72907f37..61438e8a 100644
--- a/backends/__init__.py
+++ b/backends/__init__.py
@@ -1,3 +1,7 @@
+"""
+Backends that extend U1DB functionality.
+"""
+
import objectstore
diff --git a/backends/couch.py b/backends/couch.py
index 30fd449c..7c884aee 100644
--- a/backends/couch.py
+++ b/backends/couch.py
@@ -1,3 +1,5 @@
+"""A U1DB backend that uses CouchDB as its persistence layer."""
+
# general imports
import uuid
from base64 import b64encode, b64decode
@@ -22,14 +24,16 @@ except ImportError:
class InvalidURLError(Exception):
+ """Exception raised when Soledad encounters a malformed URL."""
pass
class CouchDatabase(ObjectStore):
- """A U1DB implementation that uses Couch as its persistence layer."""
+ """A U1DB backend that uses Couch as its persistence layer."""
@classmethod
def open_database(cls, url, create):
+ """Open a U1DB database using CouchDB as backend."""
# get database from url
m = re.match('(^https?://[^/]+)/(.+)$', url)
if not m:
@@ -69,9 +73,7 @@ class CouchDatabase(ObjectStore):
#-------------------------------------------------------------------------
def _get_doc(self, doc_id, check_for_conflicts=False):
- """
- Get just the document content, without fancy handling.
- """
+ """Get just the document content, without fancy handling."""
cdoc = self._database.get(doc_id)
if cdoc is None:
return None
@@ -90,7 +92,7 @@ class CouchDatabase(ObjectStore):
return doc
def get_all_docs(self, include_deleted=False):
- """Get all documents from the database."""
+ """Get the JSON content for all documents in the database."""
generation = self._get_generation()
results = []
for doc_id in self._database:
@@ -103,6 +105,7 @@ class CouchDatabase(ObjectStore):
return (generation, results)
def _put_doc(self, doc):
+ """Store document in database."""
# prepare couch's Document
cdoc = CouchDocument()
cdoc['_id'] = doc.doc_id
@@ -122,9 +125,15 @@ class CouchDatabase(ObjectStore):
self._database.delete_attachment(cdoc, 'u1db_json')
def get_sync_target(self):
+ """
+ Return a SyncTarget object, for another u1db to synchronize with.
+ """
return CouchSyncTarget(self)
def create_index(self, index_name, *index_expressions):
+ """
+ Create a named index, which can then be queried for future lookups.
+ """
if index_name in self._indexes:
if self._indexes[index_name]._definition == list(
index_expressions):
@@ -142,6 +151,7 @@ class CouchDatabase(ObjectStore):
self._store_u1db_data()
def close(self):
+ """Release any resources associated with this database."""
# TODO: fix this method so the connection is properly closed and
# test_close (+tearDown, which deletes the db) works without problems.
self._url = None
@@ -152,6 +162,7 @@ class CouchDatabase(ObjectStore):
return True
def sync(self, url, creds=None, autocreate=True):
+ """Synchronize documents with remote replica exposed at url."""
from u1db.sync import Synchronizer
return Synchronizer(self, CouchSyncTarget(url, creds=creds)).sync(
autocreate=autocreate)
@@ -206,6 +217,7 @@ class CouchDatabase(ObjectStore):
#-------------------------------------------------------------------------
def delete_database(self):
+ """Delete a U1DB CouchDB database."""
del(self._server[self._dbname])
def _dump_indexes_as_json(self):
@@ -228,6 +240,7 @@ class CouchDatabase(ObjectStore):
class CouchSyncTarget(LocalSyncTarget):
def get_sync_info(self, source_replica_uid):
+ """Return information about known state."""
source_gen, source_trans_id = self._db._get_replica_gen_and_trans_id(
source_replica_uid)
my_gen, my_trans_id = self._db._get_generation_info()
@@ -237,6 +250,7 @@ class CouchSyncTarget(LocalSyncTarget):
def record_sync_info(self, source_replica_uid, source_replica_generation,
source_replica_transaction_id):
+ """Record tip information for another replica."""
if self._trace_hook:
self._trace_hook('record_sync_info')
self._db._set_replica_gen_and_trans_id(
@@ -245,25 +259,26 @@ class CouchSyncTarget(LocalSyncTarget):
class CouchServerState(ServerState):
- """
- Inteface of the WSGI server with the CouchDB backend.
- """
+ """Inteface of the WSGI server with the CouchDB backend."""
def __init__(self, couch_url):
self.couch_url = couch_url
def open_database(self, dbname):
+ """Open a database at the given location."""
# TODO: open couch
from leap.soledad.backends.couch import CouchDatabase
return CouchDatabase.open_database(self.couch_url + '/' + dbname,
create=False)
def ensure_database(self, dbname):
+ """Ensure database at the given location."""
from leap.soledad.backends.couch import CouchDatabase
db = CouchDatabase.open_database(self.couch_url + '/' + dbname,
create=True)
return db, db._replica_uid
def delete_database(self, dbname):
+ """Delete database at the given location."""
from leap.soledad.backends.couch import CouchDatabase
CouchDatabase.delete_database(self.couch_url + '/' + dbname)
diff --git a/backends/leap_backend.py b/backends/leap_backend.py
index c3c52ee6..571cd8ca 100644
--- a/backends/leap_backend.py
+++ b/backends/leap_backend.py
@@ -1,3 +1,8 @@
+"""
+A U1DB backend that encrypts data before sending to server and decrypts after
+receiving.
+"""
+
try:
import simplejson as json
except ImportError:
@@ -13,14 +18,23 @@ import uuid
class NoDefaultKey(Exception):
+ """
+ Exception to signal that there's no default OpenPGP key configured.
+ """
pass
class NoSoledadInstance(Exception):
+ """
+ Exception to signal that no Soledad instance was found.
+ """
pass
class DocumentEncryptionFailed(Exception):
+ """
+ Exception to signal the failure of document encryption.
+ """
pass
diff --git a/backends/objectstore.py b/backends/objectstore.py
index d7aa3049..1ac03df4 100644
--- a/backends/objectstore.py
+++ b/backends/objectstore.py
@@ -1,3 +1,11 @@
+"""
+Abstract U1DB backend to handle storage using object stores (like CouchDB, for
+example.
+
+Right now, this is only used by CouchDatabase backend, but can also be
+extended to implement OpenStack or Amazon S3 storage, for example.
+"""
+
from u1db.backends.inmemory import InMemoryDatabase
from u1db import errors
@@ -37,6 +45,7 @@ class ObjectStore(InMemoryDatabase):
raise NotImplementedError(self.get_all_docs)
def delete_doc(self, doc):
+ """Mark a document as deleted."""
old_doc = self._get_doc(doc.doc_id, check_for_conflicts=True)
if old_doc is None:
raise errors.DocumentDoesNotExist
@@ -55,9 +64,13 @@ class ObjectStore(InMemoryDatabase):
# index-related methods
def create_index(self, index_name, *index_expressions):
+ """
+ Create an named index, which can then be queried for future lookups.
+ """
raise NotImplementedError(self.create_index)
def delete_index(self, index_name):
+ """Remove a named index."""
super(ObjectStore, self).delete_index(index_name)
self._store_u1db_data()
diff --git a/backends/sqlcipher.py b/backends/sqlcipher.py
index c902b466..9a508dc2 100644
--- a/backends/sqlcipher.py
+++ b/backends/sqlcipher.py
@@ -14,7 +14,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with u1db. If not, see <http://www.gnu.org/licenses/>.
-"""A U1DB implementation that uses SQLCipher as its persistence layer."""
+"""A U1DB backend that uses SQLCipher as its persistence layer."""
import os
from pysqlcipher import dbapi2
@@ -125,6 +125,7 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase):
@classmethod
def open_database(cls, sqlite_file, password, create, backend_cls=None,
document_factory=None, soledad=None):
+ """Open U1DB database using SQLCipher as backend."""
try:
return cls._open_database(sqlite_file, password,
document_factory=document_factory,
diff --git a/server.py b/server.py
index 4fc97be5..9e43de93 100644
--- a/server.py
+++ b/server.py
@@ -1,5 +1,5 @@
"""
-An u1db server that stores data using couchdb.
+A U1DB server that stores data using couchdb.
This should be run with:
twistd -n web --wsgi=leap.soledad.server.application
@@ -10,6 +10,7 @@ from twisted.internet import reactor
from u1db.remote import http_app
from leap.soledad.backends.couch import CouchServerState
+# TODO: change couch url accordingly
couch_url = 'http://localhost:5984'
state = CouchServerState(couch_url)
# TODO: change working dir to something meaningful
diff --git a/util.py b/util.py
index 4bc4d2c9..56d1a9a0 100644
--- a/util.py
+++ b/util.py
@@ -1,3 +1,7 @@
+"""
+Utilities for Soledad.
+"""
+
import os
import gnupg
import re
@@ -28,6 +32,9 @@ class GPGWrapper(gnupg.GPG):
def encrypt(self, data, recipient, sign=None, always_trust=True,
passphrase=None, symmetric=False):
+ """
+ Encrypt data using GPG.
+ """
# TODO: devise a way so we don't need to "always trust".
return super(GPGWrapper, self).encrypt(data, recipient, sign=sign,
always_trust=always_trust,
@@ -35,6 +42,9 @@ class GPGWrapper(gnupg.GPG):
symmetric=symmetric)
def decrypt(self, data, always_trust=True, passphrase=None):
+ """
+ Decrypt data using GPG.
+ """
# TODO: devise a way so we don't need to "always trust".
return super(GPGWrapper, self).decrypt(data,
always_trust=always_trust,