diff options
Diffstat (limited to 'soledad/backends/sqlcipher.py')
-rw-r--r-- | soledad/backends/sqlcipher.py | 163 |
1 files changed, 163 insertions, 0 deletions
diff --git a/soledad/backends/sqlcipher.py b/soledad/backends/sqlcipher.py new file mode 100644 index 00000000..5d2569bf --- /dev/null +++ b/soledad/backends/sqlcipher.py @@ -0,0 +1,163 @@ +"""A U1DB backend that uses SQLCipher as its persistence layer.""" + +import os +from pysqlcipher import dbapi2 +import time + +from leap import util +from u1db.backends import sqlite_backend +util.logger.debug( + "Monkey-patching u1db.backends.sqlite_backend with pysqlcipher.dbapi2..." +) +sqlite_backend.dbapi2 = dbapi2 + +from u1db import ( + errors, +) + +from leap.soledad.backends.leap_backend import LeapDocument + + +def open(path, password, create=True, document_factory=None, soledad=None): + """Open a database at the given location. + + Will raise u1db.errors.DatabaseDoesNotExist if create=False and the + database does not already exist. + + :param path: The filesystem path for the database to open. + :param create: True/False, should the database be created if it doesn't + already exist? + :param document_factory: A function that will be called with the same + parameters as Document.__init__. + :return: An instance of Database. + """ + return SQLCipherDatabase.open_database( + path, password, create=create, document_factory=document_factory, + soledad=soledad) + + +class DatabaseIsNotEncrypted(Exception): + """ + Exception raised when trying to open non-encrypted databases. + """ + pass + + +class SQLCipherDatabase(sqlite_backend.SQLitePartialExpandDatabase): + """A U1DB implementation that uses SQLCipher as its persistence layer.""" + + _index_storage_value = 'expand referenced encrypted' + + @classmethod + def set_pragma_key(cls, db_handle, key): + db_handle.cursor().execute("PRAGMA key = '%s'" % key) + + def __init__(self, sqlite_file, password, document_factory=None, + soledad=None): + """Create a new sqlcipher file.""" + self._check_if_db_is_encrypted(sqlite_file) + self._db_handle = dbapi2.connect(sqlite_file) + SQLCipherDatabase.set_pragma_key(self._db_handle, password) + self._real_replica_uid = None + self._ensure_schema() + self._soledad = soledad + + def factory(doc_id=None, rev=None, json='{}', has_conflicts=False, + encrypted_json=None, syncable=True): + return LeapDocument(doc_id=doc_id, rev=rev, json=json, + has_conflicts=has_conflicts, + encrypted_json=encrypted_json, + syncable=syncable, soledad=self._soledad) + self.set_document_factory(factory) + + def _check_if_db_is_encrypted(self, sqlite_file): + if not os.path.exists(sqlite_file): + return + else: + try: + # try to open an encrypted database with the regular u1db + # backend should raise a DatabaseError exception. + sqlite_backend.SQLitePartialExpandDatabase(sqlite_file) + raise DatabaseIsNotEncrypted() + except dbapi2.DatabaseError: + pass + + @classmethod + def _open_database(cls, sqlite_file, password, document_factory=None, + soledad=None): + if not os.path.isfile(sqlite_file): + raise errors.DatabaseDoesNotExist() + tries = 2 + while True: + # Note: There seems to be a bug in sqlite 3.5.9 (with python2.6) + # where without re-opening the database on Windows, it + # doesn't see the transaction that was just committed + db_handle = dbapi2.connect(sqlite_file) + SQLCipherDatabase.set_pragma_key(db_handle, password) + c = db_handle.cursor() + v, err = cls._which_index_storage(c) + db_handle.close() + if v is not None: + break + # possibly another process is initializing it, wait for it to be + # done + if tries == 0: + raise err # go for the richest error? + tries -= 1 + time.sleep(cls.WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL) + return SQLCipherDatabase._sqlite_registry[v]( + sqlite_file, password, document_factory=document_factory, + soledad=soledad) + + @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, + soledad=soledad) + except errors.DatabaseDoesNotExist: + if not create: + raise + if backend_cls is None: + # default is SQLCipherPartialExpandDatabase + backend_cls = SQLCipherDatabase + return backend_cls(sqlite_file, password, + document_factory=document_factory, + soledad=soledad) + + def sync(self, url, creds=None, autocreate=True): + """ + Synchronize encrypted documents with remote replica exposed at url. + """ + from u1db.sync import Synchronizer + from leap.soledad.backends.leap_backend import LeapSyncTarget + return Synchronizer( + self, + LeapSyncTarget(url, + creds=creds, + soledad=self._soledad)).sync(autocreate=autocreate) + + def _extra_schema_init(self, c): + c.execute( + 'ALTER TABLE document ' + 'ADD COLUMN syncable BOOL NOT NULL DEFAULT TRUE') + + def _put_and_update_indexes(self, old_doc, doc): + super(SQLCipherDatabase, self)._put_and_update_indexes(old_doc, doc) + c = self._db_handle.cursor() + c.execute('UPDATE document SET syncable=? WHERE doc_id=?', + (doc.syncable, doc.doc_id)) + + def _get_doc(self, doc_id, check_for_conflicts=False): + doc = super(SQLCipherDatabase, self)._get_doc(doc_id, + check_for_conflicts) + if doc: + c = self._db_handle.cursor() + c.execute('SELECT syncable FROM document WHERE doc_id=?', + (doc.doc_id,)) + doc.syncable = bool(c.fetchone()[0]) + return doc + +sqlite_backend.SQLiteDatabase.register_implementation(SQLCipherDatabase) |