diff options
Diffstat (limited to 'src/leap/soledad/backends/sqlcipher.py')
-rw-r--r-- | src/leap/soledad/backends/sqlcipher.py | 162 |
1 files changed, 142 insertions, 20 deletions
diff --git a/src/leap/soledad/backends/sqlcipher.py b/src/leap/soledad/backends/sqlcipher.py index ab74bab1..9e3c38c9 100644 --- a/src/leap/soledad/backends/sqlcipher.py +++ b/src/leap/soledad/backends/sqlcipher.py @@ -1,3 +1,21 @@ +# -*- coding: utf-8 -*- +# sqlcipher.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + """A U1DB backend that uses SQLCipher as its persistence layer.""" import os @@ -26,12 +44,17 @@ def open(path, password, create=True, document_factory=None, soledad=None): 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 + @param path: The filesystem path for the database to open. + @param type: str + @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 + @param type: bool + @param document_factory: A function that will be called with the same parameters as Document.__init__. - :return: An instance of Database. + @type document_factory: callable + + @return: An instance of Database. + @rtype SQLCipherDatabase """ return SQLCipherDatabase.open_database( path, password, create=create, document_factory=document_factory, @@ -54,11 +77,24 @@ class SQLCipherDatabase(sqlite_backend.SQLitePartialExpandDatabase): 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, + def __init__(self, sqlcipher_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) + """ + Create a new sqlcipher file. + + @param sqlcipher_file: The path for the SQLCipher file. + @type sqlcipher_file: str + @param password: The password that protects the SQLCipher db. + @type password: str + @param document_factory: A function that will be called with the same + parameters as Document.__init__. + @type document_factory: callable + @param soledad: An instance of Soledad so we can encrypt/decrypt + document contents when syncing. + @type soledad: soledad.Soledad + """ + self._check_if_db_is_encrypted(sqlcipher_file) + self._db_handle = dbapi2.connect(sqlcipher_file) SQLCipherDatabase.set_pragma_key(self._db_handle, password) self._real_replica_uid = None self._ensure_schema() @@ -72,29 +108,55 @@ class SQLCipherDatabase(sqlite_backend.SQLitePartialExpandDatabase): 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): + def _check_if_db_is_encrypted(self, sqlcipher_file): + """ + Verify if loca file is an encrypted database. + + @param sqlcipher_file: The path for the SQLCipher file. + @type sqlcipher_file: str + + @return: True if the database is encrypted, False otherwise. + @rtype: bool + """ + if not os.path.exists(sqlcipher_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) + sqlite_backend.SQLitePartialExpandDatabase(sqlcipher_file) raise DatabaseIsNotEncrypted() except dbapi2.DatabaseError: pass @classmethod - def _open_database(cls, sqlite_file, password, document_factory=None, + def _open_database(cls, sqlcipher_file, password, document_factory=None, soledad=None): - if not os.path.isfile(sqlite_file): + """ + Open a SQLCipher database. + + @param sqlcipher_file: The path for the SQLCipher file. + @type sqlcipher_file: str + @param password: The password that protects the SQLCipher db. + @type password: str + @param document_factory: A function that will be called with the same + parameters as Document.__init__. + @type document_factory: callable + @param soledad: An instance of Soledad so we can encrypt/decrypt + document contents when syncing. + @type soledad: soledad.Soledad + + @return: The database object. + @rtype: SQLCipherDatabase + """ + if not os.path.isfile(sqlcipher_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) + db_handle = dbapi2.connect(sqlcipher_file) SQLCipherDatabase.set_pragma_key(db_handle, password) c = db_handle.cursor() v, err = cls._which_index_storage(c) @@ -108,30 +170,63 @@ class SQLCipherDatabase(sqlite_backend.SQLitePartialExpandDatabase): tries -= 1 time.sleep(cls.WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL) return SQLCipherDatabase._sqlite_registry[v]( - sqlite_file, password, document_factory=document_factory, + sqlcipher_file, password, document_factory=document_factory, soledad=soledad) @classmethod - def open_database(cls, sqlite_file, password, create, backend_cls=None, + def open_database(cls, sqlcipher_file, password, create, backend_cls=None, document_factory=None, soledad=None): - """Open U1DB database using SQLCipher as backend.""" + """ + Open a SQLCipher database. + + @param sqlcipher_file: The path for the SQLCipher file. + @type sqlcipher_file: str + @param password: The password that protects the SQLCipher db. + @type password: str + @param create: Should the datbase be created if it does not already + exist? + @type: bool + @param backend_cls: A class to use as backend. + @type backend_cls: type + @param document_factory: A function that will be called with the same + parameters as Document.__init__. + @type document_factory: callable + @param soledad: An instance of Soledad so we can encrypt/decrypt + document contents when syncing. + @type soledad: soledad.Soledad + + @return: The database object. + @rtype: SQLCipherDatabase + """ try: - return cls._open_database(sqlite_file, password, + return cls._open_database(sqlcipher_file, password, document_factory=document_factory, soledad=soledad) except errors.DatabaseDoesNotExist: if not create: raise + # TODO: remove backend class from here. if backend_cls is None: # default is SQLCipherPartialExpandDatabase backend_cls = SQLCipherDatabase - return backend_cls(sqlite_file, password, + return backend_cls(sqlcipher_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. + Synchronize documents with remote replica exposed at url. + + @param url: The url of the target replica to sync with. + @type url: str + @param creds: optional dictionary giving credentials. + to authorize the operation with the server. + @type creds: dict + @param autocreate: Ask the target to create the db if non-existent. + @type autocreate: bool + + @return: The local generation before the synchronisation was performed. + @rtype: int """ from u1db.sync import Synchronizer from leap.soledad.backends.leap_backend import LeapSyncTarget @@ -142,17 +237,44 @@ class SQLCipherDatabase(sqlite_backend.SQLitePartialExpandDatabase): soledad=self._soledad)).sync(autocreate=autocreate) def _extra_schema_init(self, c): + """ + Add any extra fields, etc to the basic table definitions. + + @param c: The cursor for querying the database. + @type c: dbapi2.cursor + """ c.execute( 'ALTER TABLE document ' 'ADD COLUMN syncable BOOL NOT NULL DEFAULT TRUE') def _put_and_update_indexes(self, old_doc, doc): + """ + Update a document and all indexes related to it. + + @param old_doc: The old version of the document. + @type old_doc: u1db.Document + @param doc: The new version of the document. + @type doc: u1db.Document + """ 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): + """ + Get just the document content, without fancy handling. + + @param doc_id: The unique document identifier + @type doc_id: str + @param include_deleted: If set to True, deleted documents will be + returned with empty content. Otherwise asking for a deleted + document will return None. + @type include_deleted: bool + + @return: a Document object. + @type: u1db.Document + """ doc = super(SQLCipherDatabase, self)._get_doc(doc_id, check_for_conflicts) if doc: |