diff options
-rw-r--r-- | __init__.py | 39 | ||||
-rw-r--r-- | backends/__init__.py | 4 | ||||
-rw-r--r-- | backends/couch.py | 31 | ||||
-rw-r--r-- | backends/leap_backend.py | 14 | ||||
-rw-r--r-- | backends/objectstore.py | 13 | ||||
-rw-r--r-- | backends/sqlcipher.py | 3 | ||||
-rw-r--r-- | server.py | 3 | ||||
-rw-r--r-- | util.py | 10 |
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, @@ -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 @@ -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, |