From aa60792075355dc245209baed20aefb9d9b66819 Mon Sep 17 00:00:00 2001 From: Micah Anderson Date: Tue, 13 Aug 2013 14:42:30 -0400 Subject: update to 0.3.0 --- CHANGELOG | 9 + MANIFEST.in | 1 + README.rst | 28 ++++ debian/changelog | 6 + soledad/setup.py | 2 +- soledad/src/leap/soledad/__init__.py | 102 ++++++------ soledad/src/leap/soledad/dbwrapper.py | 183 +++++++++++++++++++++ soledad_server/pkg/soledad | 14 +- soledad_server/setup.py | 5 +- soledad_server/src/leap/soledad_server/__init__.py | 2 +- soledad_server/src/leap/soledad_server/auth.py | 4 +- 11 files changed, 285 insertions(+), 71 deletions(-) create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 soledad/src/leap/soledad/dbwrapper.py diff --git a/CHANGELOG b/CHANGELOG index 3338d5b6..057a46cc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +0.3.0 Aug 9: +Client: + o Thread safe wrapper for pysqlcipher. + o Fix a couple of typos that prevented certain functionality to + work. Fixes #3306 +Server: + o A plaintext port is not opened by soledad server initscript call + to twistd web anymore. Closes #3254. + 0.2.3 Jul 26: Client: o Avoid possible timing attack in document's mac comparison by diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..152a4ce6 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include pkg/soledad diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..fa953f05 --- /dev/null +++ b/README.rst @@ -0,0 +1,28 @@ +Soledad +================================================================== +*Synchronization Of Locally Encrypted Data Among Devices* + +.. image:: https://pypip.in/v/leap.soledad/badge.png + :target: https://crate.io/packages/leap.soledad + +This software is under development. + +Tests +----- + +Client and server tests are both included in leap.soledad. Because +soledad_server depends on soledad and soledad tests depend on soledad_server, +if you want to run tests in development mode you must first install soledad, +then soledad_server, and then run the tests. + +Therefore, tests must be run with:: + + cd soledad + python setup.py develop + cd ../soledad_server + python setup.py develop + cd ../soledad + python setup.py test + +Note that to run CouchDB tests, be sure you have ``CouchDB`` installed on your +system. diff --git a/debian/changelog b/debian/changelog index b13f6c2a..766459f3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +soledad (0.3.0) unstable; urgency=low + + * Update to 0.3.0 + + -- Micah Anderson Tue, 13 Aug 2013 14:42:06 -0400 + soledad (0.2.3) unstable; urgency=low * Upgrade to new release diff --git a/soledad/setup.py b/soledad/setup.py index 3e46c353..f2291662 100644 --- a/soledad/setup.py +++ b/soledad/setup.py @@ -62,7 +62,7 @@ trove_classifiers = ( setup( name='leap.soledad', - version='0.2.3', + version='0.3.0', url='https://leap.se/', license='GPLv3+', description='Synchronization of locally encrypted data among devices.', diff --git a/soledad/src/leap/soledad/__init__.py b/soledad/src/leap/soledad/__init__.py index 956f47a7..00ac21f8 100644 --- a/soledad/src/leap/soledad/__init__.py +++ b/soledad/src/leap/soledad/__init__.py @@ -27,7 +27,6 @@ remote storage in the server side. """ import os -import string import binascii import logging import urlparse @@ -42,69 +41,56 @@ import errno from xdg import BaseDirectory from hashlib import sha256 from u1db.remote import http_client -from u1db.remote.ssl_match_hostname import ( # noqa - CertificateError, - match_hostname, -) +from u1db.remote.ssl_match_hostname import match_hostname # # Assert functions # -def soledad_assert(condition, message): - """ - Asserts the condition and displays the message if that's not - met. - - @param condition: condition to check - @type condition: bool - @param message: message to display if the condition isn't met - @type message: str - """ - assert condition, message - - # we want to use leap.common.check.leap_assert in case it is available, # because it also logs in a way other parts of leap can access log messages. + try: - from leap.common.check import leap_assert - soledad_assert = leap_assert -except ImportError: - pass + from leap.common.check import leap_assert as soledad_assert +except ImportError: -def soledad_assert_type(var, expectedType): - """ - Helper assert check for a variable's expected type + def soledad_assert(condition, message): + """ + Asserts the condition and displays the message if that's not + met. - @param var: variable to check - @type var: any - @param expectedType: type to check agains - @type expectedType: type - """ - soledad_assert(isinstance(var, expectedType), - "Expected type %r instead of %r" % - (expectedType, type(var))) + @param condition: condition to check + @type condition: bool + @param message: message to display if the condition isn't met + @type message: str + """ + assert condition, message try: - from leap.common.check import leap_assert_type - soledad_assert_type = leap_assert_type + from leap.common.check import leap_assert_type as soledad_assert_type + except ImportError: - pass + + def soledad_assert_type(var, expectedType): + """ + Helper assert check for a variable's expected type + + @param var: variable to check + @type var: any + @param expectedType: type to check agains + @type expectedType: type + """ + soledad_assert(isinstance(var, expectedType), + "Expected type %r instead of %r" % + (expectedType, type(var))) # # Signaling function # -# we define a fake signaling function and fake signal constants that will -# allow for logging signaling attempts in case leap.common.events is not -# available. - -def signal(signal, content=""): - logger.info("Would signal: %s - %s." % (str(signal), content)) - SOLEDAD_CREATING_KEYS = 'Creating keys...' SOLEDAD_DONE_CREATING_KEYS = 'Done creating keys.' SOLEDAD_DOWNLOADING_KEYS = 'Downloading keys...' @@ -117,9 +103,7 @@ SOLEDAD_DONE_DATA_SYNC = 'Done data sync.' # we want to use leap.common.events to emits signals, if it is available. try: from leap.common import events - # replace fake signaling function with real one - signal = events.signal - # replace fake string signals with real signals + from leap.common.events import signal SOLEDAD_CREATING_KEYS = events.events_pb2.SOLEDAD_CREATING_KEYS SOLEDAD_DONE_CREATING_KEYS = events.events_pb2.SOLEDAD_DONE_CREATING_KEYS SOLEDAD_DOWNLOADING_KEYS = events.events_pb2.SOLEDAD_DOWNLOADING_KEYS @@ -130,18 +114,23 @@ try: events.events_pb2.SOLEDAD_DONE_UPLOADING_KEYS SOLEDAD_NEW_DATA_TO_SYNC = events.events_pb2.SOLEDAD_NEW_DATA_TO_SYNC SOLEDAD_DONE_DATA_SYNC = events.events_pb2.SOLEDAD_DONE_DATA_SYNC + except ImportError: - pass + # we define a fake signaling function and fake signal constants that will + # allow for logging signaling attempts in case leap.common.events is not + # available. + + def signal(signal, content=""): + logger.info("Would signal: %s - %s." % (str(signal), content)) +from leap.soledad.crypto import SoledadCrypto +from leap.soledad.dbwrapper import SQLCipherWrapper from leap.soledad.document import SoledadDocument -from leap.soledad.sqlcipher import ( - open as sqlcipher_open, - SQLCipherDatabase, -) -from leap.soledad.target import SoledadSyncTarget from leap.soledad.shared_db import SoledadSharedDatabase -from leap.soledad.crypto import SoledadCrypto +from leap.soledad.sqlcipher import open as sqlcipher_open +from leap.soledad.sqlcipher import SQLCipherDatabase +from leap.soledad.target import SoledadSyncTarget logger = logging.getLogger(name=__name__) @@ -439,7 +428,9 @@ class Soledad(object): secret[salt_start:salt_end], # the salt buflen=32, # we need a key with 256 bits (32 bytes) ) - self._db = sqlcipher_open( + + # Instantiate a thread-safe wrapper + self._db = SQLCipherWrapper( self._local_db_path, binascii.b2a_hex(key), # sqlcipher only accepts the hex version create=True, @@ -453,13 +444,14 @@ class Soledad(object): """ if hasattr(self, '_db') and isinstance( self._db, - SQLCipherDatabase): + SQLCipherWrapper): self._db.close() def __del__(self): """ Make sure local database is closed when object is destroyed. """ + # Watch out! We have no guarantees that this is properly called. self.close() # diff --git a/soledad/src/leap/soledad/dbwrapper.py b/soledad/src/leap/soledad/dbwrapper.py new file mode 100644 index 00000000..c16c4925 --- /dev/null +++ b/soledad/src/leap/soledad/dbwrapper.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# dbwrapper.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 . +""" +Thread-safe wrapper for sqlite/pysqlcipher. + +*TODO* +At some point we surely will want to switch to a twisted way of dealing +with this, using defers and proper callbacks. But I had this tested for +some time so postponing that refactor. +""" +import logging +import threading +import Queue +import time + +import exceptions + +from functools import partial + +from leap.soledad import sqlcipher + +logger = logging.getLogger(__name__) + + +class SQLCipherWrapper(threading.Thread): + + def __init__(self, *args, **kwargs): + """ + Initializes a wrapper that proxies method and attribute + access to an underlying SQLCipher instance. We instantiate sqlcipher + in a thread, and all method accesses communicate with it using a + Queue. + + :param *args: position arguments to pass to pysqlcipher initialization + :type args: tuple + + :param **kwargs: keyword arguments to pass to pysqlcipher + initialization + :type kwargs: dict + """ + threading.Thread.__init__(self) + self._db = None + self._wrargs = args, kwargs + + self._queue = Queue.Queue() + self._stopped = threading.Event() + + self.start() + + def _init_db(self): + """ + Initializes sqlcipher database. + + This is called on a separate thread. + """ + # instantiate u1db + args, kwargs = self._wrargs + self._db = sqlcipher.open(*args, **kwargs) + + def run(self): + """ + Main loop for the sqlcipher thread. + """ + logger.debug("SQLCipherWrapper thread started.") + logger.debug("Initializing sqlcipher") + end_mths = ("__end_thread", "_SQLCipherWrapper__end_thread") + + self._init_db() + self._lock = threading.Lock() + + ct = 0 + started = False + + while True: + if self._db is None: + if started: + break + if ct > 10: + break # XXX DEBUG + logger.debug('db not ready yet, waiting...') + time.sleep(1) + ct += 1 + + started = True + + with self._lock: + try: + mth, q, wrargs = self._queue.get() + except: + logger.error("exception getting args from queue") + + res = None + attr = getattr(self._db, mth, None) + if not attr: + if mth not in end_mths: + logger.error('method %s does not exist' % (mth,)) + res = AttributeError( + "_db instance has no attribute %s" % mth) + + elif callable(attr): + # invoke the method with the passed args + args = wrargs.get('args', []) + kwargs = wrargs.get('kwargs', {}) + try: + res = attr(*args, **kwargs) + except Exception as e: + logger.error( + "Error on proxied method %s: '%r'." % ( + attr, e)) + res = e + else: + # non-callable attribute + res = attr + logger.debug('returning proxied db call...') + q.put(res) + + if mth in end_mths: + logger.debug('ending thread') + break + + logger.debug("SQLCipherWrapper thread terminated.") + self._stopped.set() + + def close(self): + """ + Closes the sqlcipher database and finishes the thread. This method + should always be called explicitely. + """ + self.__getattr__('close')() + self.__end_thread() + + def __getattr__(self, attr): + """ + Returns _db proxied attributes. + """ + + def __proxied_mth(method, *args, **kwargs): + if not self._stopped.isSet(): + wrargs = {'args': args, 'kwargs': kwargs} + q = Queue.Queue() + self._queue.put((method, q, wrargs)) + res = q.get() + q.task_done() + + if isinstance(res, exceptions.BaseException): + # XXX should get the original bt + raise res + return res + else: + logger.warning("tried to call proxied meth " + "but stopped is set: %s" % + (method,)) + + rgetattr = object.__getattribute__ + + if attr != "_db": + proxied = partial(__proxied_mth, attr) + return proxied + + # fallback to regular behavior + return rgetattr(self, attr) + + def __del__(self): + """ + Do not trust this get called. No guarantees given. Because of a funny + dance with the refs and the way the gc works, we should be calling the + close method explicitely. + """ + self.close() diff --git a/soledad_server/pkg/soledad b/soledad_server/pkg/soledad index 1254cabe..c233731e 100644 --- a/soledad_server/pkg/soledad +++ b/soledad_server/pkg/soledad @@ -15,7 +15,6 @@ RUNDIR=/var/lib/soledad/ OBJ=leap.soledad_server.application LOGFILE=/var/log/soledad.log HTTPS_PORT=2424 -PLAIN_PORT=65534 CERT_PATH=/etc/leap/soledad-server.pem PRIVKEY_PATH=/etc/leap/soledad-server.key TWISTD_PATH=/usr/bin/twistd @@ -32,14 +31,11 @@ case "$1" in start) echo -n "Starting soledad: twistd" start-stop-daemon --start --quiet --exec $TWISTD_PATH -- \ - --pidfile=$PIDFILE \ - --logfile=$LOGFILE \ - web \ - --wsgi=$OBJ \ - --https=$HTTPS_PORT \ - --certificate=$CERT_PATH \ - --privkey=$PRIVKEY_PATH \ - --port=$PLAIN_PORT + --pidfile=$PIDFILE \ + --logfile=$LOGFILE \ + web \ + --wsgi=$OBJ \ + --port=ssl:$HTTPS_PORT:privateKey=$PRIVKEY_PATH:certKey=$CERT_PATH echo "." ;; diff --git a/soledad_server/setup.py b/soledad_server/setup.py index f022c9cf..16cebca1 100644 --- a/soledad_server/setup.py +++ b/soledad_server/setup.py @@ -34,7 +34,7 @@ install_requirements = [ 'six==1.1.0', 'routes', 'PyOpenSSL', - 'leap.soledad>=0.2.3', + 'leap.soledad>=0.3.0', ] @@ -60,7 +60,7 @@ trove_classifiers = ( setup( name='leap.soledad_server', - version='0.2.3', + version='0.3.0', url='https://leap.se/', license='GPLv3+', description='Synchronization of locally encrypted data among devices.', @@ -75,6 +75,5 @@ setup( packages=find_packages('src'), package_dir={'': 'src'}, install_requires=install_requirements, - data_files=data_files, classifiers=trove_classifiers, ) diff --git a/soledad_server/src/leap/soledad_server/__init__.py b/soledad_server/src/leap/soledad_server/__init__.py index 79609d7d..18a0546e 100644 --- a/soledad_server/src/leap/soledad_server/__init__.py +++ b/soledad_server/src/leap/soledad_server/__init__.py @@ -122,4 +122,4 @@ state = CouchServerState(conf['couch_url']) # WSGI application that may be used by `twistd -web` application = SoledadTokenAuthMiddleware(SoledadApp(state)) -resource = WSGIResource(reactor, reactor.getThreadPool(), application) +resource = WSGIResource(reactor, reactor.getThreadPool(), application) \ No newline at end of file diff --git a/soledad_server/src/leap/soledad_server/auth.py b/soledad_server/src/leap/soledad_server/auth.py index e0169523..3bcfcf04 100644 --- a/soledad_server/src/leap/soledad_server/auth.py +++ b/soledad_server/src/leap/soledad_server/auth.py @@ -343,8 +343,8 @@ class SoledadAuthMiddleware(object): @rtype: bool """ return URLToAuthorization( - uuid, self.app.SHARED_DB_NAME, - self.app.USER_DB_PREFIX + uuid, self._app.SHARED_DB_NAME, + self._app.USER_DB_PREFIX ).is_authorized(environ) @abstractmethod -- cgit v1.2.3