diff options
-rw-r--r-- | CHANGELOG | 48 | ||||
-rw-r--r-- | MANIFEST.in | 1 | ||||
-rw-r--r-- | README.rst | 16 | ||||
-rw-r--r-- | changes/bug_fix-deps | 1 | ||||
-rw-r--r-- | changes/bug_fix-pip-install | 1 | ||||
-rw-r--r-- | changes/feature_add-action-validation | 1 | ||||
-rw-r--r-- | changes/feature_add-mac-authentication | 1 | ||||
-rw-r--r-- | changes/feature_add-sqlcipher-api | 3 | ||||
-rw-r--r-- | changes/feature_add-status-to-initscript | 1 | ||||
-rw-r--r-- | changes/feature_blank-server | 1 | ||||
-rw-r--r-- | changes/feature_change-symmetric-encryption-method-to-aes-256-ctr | 1 | ||||
-rw-r--r-- | changes/feature_encode-all-u1db-data-in-couch-backend | 1 | ||||
-rw-r--r-- | changes/feature_encrypt-storage-key-with-kdf | 6 | ||||
-rw-r--r-- | debian/changelog | 2 | ||||
-rw-r--r-- | debian/compat | 2 | ||||
-rw-r--r-- | debian/control | 26 | ||||
-rw-r--r-- | debian/pydist-overrides | 2 | ||||
-rwxr-xr-x | debian/rules | 11 | ||||
-rw-r--r-- | debian/soledad-server.init | 1 | ||||
-rw-r--r-- | pkg/soledad | 64 | ||||
-rw-r--r-- | soledad/setup.py (renamed from setup.py) | 24 | ||||
-rw-r--r-- | soledad/src/leap/__init__.py (renamed from src/leap/__init__.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/__init__.py (renamed from src/leap/soledad/__init__.py) | 240 | ||||
-rw-r--r-- | soledad/src/leap/soledad/auth.py (renamed from src/leap/soledad/auth.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/backends/__init__.py | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/backends/leap_backend.py | 73 | ||||
-rw-r--r-- | soledad/src/leap/soledad/crypto.py (renamed from src/leap/soledad/crypto.py) | 64 | ||||
-rw-r--r-- | soledad/src/leap/soledad/document.py | 111 | ||||
-rw-r--r-- | soledad/src/leap/soledad/shared_db.py (renamed from src/leap/soledad/shared_db.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/sqlcipher.py (renamed from src/leap/soledad/backends/sqlcipher.py) | 8 | ||||
-rw-r--r-- | soledad/src/leap/soledad/target.py (renamed from src/leap/soledad/backends/leap_backend.py) | 129 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/__init__.py (renamed from src/leap/soledad/tests/__init__.py) | 16 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/couchdb.ini.template (renamed from src/leap/soledad/tests/couchdb.ini.template) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/test_couch.py (renamed from src/leap/soledad/tests/test_couch.py) | 9 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/test_crypto.py (renamed from src/leap/soledad/tests/test_crypto.py) | 213 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/test_server.py (renamed from src/leap/soledad/tests/test_server.py) | 148 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/test_soledad.py (renamed from src/leap/soledad/tests/test_soledad.py) | 130 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/test_sqlcipher.py (renamed from src/leap/soledad/tests/test_sqlcipher.py) | 28 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/test_target.py (renamed from src/leap/soledad/tests/test_leap_backend.py) | 62 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/README (renamed from src/leap/soledad/tests/u1db_tests/README) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/__init__.py (renamed from src/leap/soledad/tests/u1db_tests/__init__.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/test_backends.py (renamed from src/leap/soledad/tests/u1db_tests/test_backends.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/test_document.py (renamed from src/leap/soledad/tests/u1db_tests/test_document.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/test_http_app.py (renamed from src/leap/soledad/tests/u1db_tests/test_http_app.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/test_http_client.py (renamed from src/leap/soledad/tests/u1db_tests/test_http_client.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/test_http_database.py (renamed from src/leap/soledad/tests/u1db_tests/test_http_database.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/test_https.py (renamed from src/leap/soledad/tests/u1db_tests/test_https.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/test_open.py (renamed from src/leap/soledad/tests/u1db_tests/test_open.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/test_remote_sync_target.py (renamed from src/leap/soledad/tests/u1db_tests/test_remote_sync_target.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py (renamed from src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/test_sync.py (renamed from src/leap/soledad/tests/u1db_tests/test_sync.py) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/testing-certs/Makefile (renamed from src/leap/soledad/tests/u1db_tests/testing-certs/Makefile) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/testing-certs/cacert.pem (renamed from src/leap/soledad/tests/u1db_tests/testing-certs/cacert.pem) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/testing-certs/testing.cert (renamed from src/leap/soledad/tests/u1db_tests/testing-certs/testing.cert) | 0 | ||||
-rw-r--r-- | soledad/src/leap/soledad/tests/u1db_tests/testing-certs/testing.key (renamed from src/leap/soledad/tests/u1db_tests/testing-certs/testing.key) | 0 | ||||
-rw-r--r-- | soledad_server/pkg/soledad | 72 | ||||
-rw-r--r-- | soledad_server/setup.py | 80 | ||||
-rw-r--r-- | soledad_server/src/leap/__init__.py | 6 | ||||
-rw-r--r-- | soledad_server/src/leap/soledad_server/__init__.py (renamed from src/leap/soledad/server.py) | 11 | ||||
-rw-r--r-- | soledad_server/src/leap/soledad_server/couch.py (renamed from src/leap/soledad/backends/couch.py) | 8 | ||||
-rw-r--r-- | soledad_server/src/leap/soledad_server/objectstore.py (renamed from src/leap/soledad/backends/objectstore.py) | 0 | ||||
-rw-r--r-- | src/leap/soledad/backends/__init__.py | 36 |
62 files changed, 1088 insertions, 570 deletions
diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 00000000..93ce5071 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,48 @@ +0.2.2 Jul 12: +Client: + o Add method for password change. +Server: + o Use the right name as the WSGI server + +0.2.1 Jun 28: +Client: + o Do not list the backends in the __init__'s __all__ to allow not + supporting couch on the client side until the code is diveded into + client and server. o Fix bad dependencies in setup.py. + o Fix broken pip install + o Database request have default timeout too high, a + soledad.SOLEDAD_TIMEOUT variable has been added in order to have + more control over this. Fixes #2713 + o Add validation and authorization of actions upon interaction with + server. + o Add MAC authentication to encrypted representation of documents. + o Add SQLCipher API to SQLCipher backend (allow for use of raw keys, + add better encrypted db assertion, add cipher, kdf_iter, + cipher_page_size and rekey PRAGMAS). + o Change symmetric encryption method to AES-256 CTR mode. + o Change the local storage of the storage secret: + * Use scrypt to derive a key for the encryption of the storage + secret. + * Store secret in a file called 'soledad.json' by default. + * Also store the salt and encryption details, as defined in the + spec. + * This change is not backwards compatible (i.e. all previously + stored secrets are incompatible with this new encryption and + storage scheme). + o Improve tests coverage. + o Split soledad client and server into two different packages. + o Use scrypt to derive the key for local encryption. + +Server: + o Add a `status` option to Soledad init script. + o Allow to initialize soledad with a blank server + o b64 encode all U1DB data in couch backend to avoid utf8 encoding + problems. + * init.d script improvements: + * Add LSB (Linux Standards Base) 3.1 compliant header + * Remove unnecessary backslashes in variable definitions + * Replace environment variables with more standard upper-cased names + * Make a TWISTD_PATH environment variable to replace hard-coded + /usr/local/bin/twistd + * Pull environment variables together into one block o Remove strict + dependency on leap.common. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 152a4ce6..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include pkg/soledad diff --git a/README.rst b/README.rst deleted file mode 100644 index b6ffea30..00000000 --- a/README.rst +++ /dev/null @@ -1,16 +0,0 @@ -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 ------ - -To run CouchDB tests, be sure you have ``CouchDB`` installed on your system. -Tests can be run with:: - - python setup.py test diff --git a/changes/bug_fix-deps b/changes/bug_fix-deps deleted file mode 100644 index 415386f8..00000000 --- a/changes/bug_fix-deps +++ /dev/null @@ -1 +0,0 @@ - o Fix bad dependencies in setup.py. diff --git a/changes/bug_fix-pip-install b/changes/bug_fix-pip-install deleted file mode 100644 index fcb58295..00000000 --- a/changes/bug_fix-pip-install +++ /dev/null @@ -1 +0,0 @@ - o Fix broken pip install diff --git a/changes/feature_add-action-validation b/changes/feature_add-action-validation deleted file mode 100644 index 57d5b90c..00000000 --- a/changes/feature_add-action-validation +++ /dev/null @@ -1 +0,0 @@ - o Add validation and authorization of actions upon interaction with server. diff --git a/changes/feature_add-mac-authentication b/changes/feature_add-mac-authentication deleted file mode 100644 index ce5a4789..00000000 --- a/changes/feature_add-mac-authentication +++ /dev/null @@ -1 +0,0 @@ - o Add MAC authentication to encrypted representation of documents. diff --git a/changes/feature_add-sqlcipher-api b/changes/feature_add-sqlcipher-api deleted file mode 100644 index 94c5aa57..00000000 --- a/changes/feature_add-sqlcipher-api +++ /dev/null @@ -1,3 +0,0 @@ - o Add SQLCipher API to SQLCipher backend (allow for use of raw keys, add - better encrypted db assertion, add cipher, kdf_iter, cipher_page_size and - rekey PRAGMAS). diff --git a/changes/feature_add-status-to-initscript b/changes/feature_add-status-to-initscript deleted file mode 100644 index ff264091..00000000 --- a/changes/feature_add-status-to-initscript +++ /dev/null @@ -1 +0,0 @@ - o Add a `status` option to Soledad init script. diff --git a/changes/feature_blank-server b/changes/feature_blank-server deleted file mode 100644 index 6e68c992..00000000 --- a/changes/feature_blank-server +++ /dev/null @@ -1 +0,0 @@ - o Allow to initialize soledad with a blank server diff --git a/changes/feature_change-symmetric-encryption-method-to-aes-256-ctr b/changes/feature_change-symmetric-encryption-method-to-aes-256-ctr deleted file mode 100644 index 8c44436a..00000000 --- a/changes/feature_change-symmetric-encryption-method-to-aes-256-ctr +++ /dev/null @@ -1 +0,0 @@ - o Change symmetric encryption method to AES-256 CTR mode. diff --git a/changes/feature_encode-all-u1db-data-in-couch-backend b/changes/feature_encode-all-u1db-data-in-couch-backend deleted file mode 100644 index 03660557..00000000 --- a/changes/feature_encode-all-u1db-data-in-couch-backend +++ /dev/null @@ -1 +0,0 @@ - o b64 encode all U1DB data in couch backend to avoid utf8 encoding problems. diff --git a/changes/feature_encrypt-storage-key-with-kdf b/changes/feature_encrypt-storage-key-with-kdf deleted file mode 100644 index f3ccf401..00000000 --- a/changes/feature_encrypt-storage-key-with-kdf +++ /dev/null @@ -1,6 +0,0 @@ - o Change the local storage of the storage secret: - * Use scrypt to derive a key for the encryption of the storage secret. - * Store secret in a file called 'soledad.json' by default. - * Also store the salt and encryption details, as defined in the spec. - * This change is not backwards compatible (i.e. all previously stored - secrets are incompatible with this new encryption and storage scheme). diff --git a/debian/changelog b/debian/changelog index 68502a14..f18b54ea 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -soledad (0.1.1) unstable; urgency=low +soledad (0.2.2) unstable; urgency=low * Initial debian package diff --git a/debian/compat b/debian/compat index 7f8f011e..ec635144 100644 --- a/debian/compat +++ b/debian/compat @@ -1 +1 @@ -7 +9 diff --git a/debian/control b/debian/control index 840a0a9a..be3c1861 100644 --- a/debian/control +++ b/debian/control @@ -1,17 +1,33 @@ Source: soledad -Maintainer: Micah Anderson <micah@debian.org> Section: python Priority: optional -Build-Depends: python-setuptools (>= 0.6b3), python-all (>= 2.6.6-3), debhelper (>= 7), - python-configparser, python-couchdb, python-leap-common, python-scrypt +Maintainer: Micah Anderson <micah@debian.org> +Build-Depends: python-setuptools (>= 0.6b3), python-all (>= 2.6.6-3), debhelper (>= 9), + python-configparser, python-couchdb, python-leap-common, python-scrypt, python-mock, + python-nose, python-testscenarios Standards-Version: 3.9.4 -Package: python-soledad +Package: soledad-server Architecture: all Depends: ${misc:Depends}, ${python:Depends}, python-configparser, python-couchdb, python-leap-common, python-scrypt, python-gnupg, python-simplejson, python-requests, - python-six, python-twisted-web, python-sqlcipher + python-six, python-twisted-web, python-sqlcipher, soledad-common +Description: Synchronization of locally encrypted data among devices. + Soledad is the part of LEAP that allows application data to be securely + shared among devices. It provides, to other parts of the LEAP client, an + API for data storage and sync. + . + This package contains the server components. + +Package: soledad-common +Architecture: all +Depends: ${misc:Depends}, ${python:Depends}, python-sqlcipher, python-pysqlite1.1, + python-simplejson, python-oauth, python-u1db, python-six, python-scrypt, + python-xdg, python-pycryptopp, python-openssl Description: Synchronization of locally encrypted data among devices. Soledad is the part of LEAP that allows application data to be securely shared among devices. It provides, to other parts of the LEAP client, an API for data storage and sync. + . + This package contains the common soledad libraries. For the server, see the + soledad-server package diff --git a/debian/pydist-overrides b/debian/pydist-overrides new file mode 100644 index 00000000..59e30938 --- /dev/null +++ b/debian/pydist-overrides @@ -0,0 +1,2 @@ +pysqlcipher python-sqlcipher +leap.soledad soledad-common
\ No newline at end of file diff --git a/debian/rules b/debian/rules index bcebae9f..a09475c0 100755 --- a/debian/rules +++ b/debian/rules @@ -1,6 +1,15 @@ #!/usr/bin/make -f %: - dh $@ --with python2 --buildsystem=python_distutils + dh $@ --with python2 + +override_dh_auto_clean: + cd soledad && python setup.py clean -a + cd soledad_server && python setup.py clean -a + +override_dh_auto_install: + cd soledad && python setup.py install --root=../debian/soledad-common + cd soledad_server && python setup.py install --root=../debian/soledad-server + diff --git a/debian/soledad-server.init b/debian/soledad-server.init new file mode 100644 index 00000000..e352c10c --- /dev/null +++ b/debian/soledad-server.init @@ -0,0 +1 @@ +../soledad_server/pkg/soledad diff --git a/pkg/soledad b/pkg/soledad deleted file mode 100644 index 036b76da..00000000 --- a/pkg/soledad +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/sh - -PATH=/sbin:/bin:/usr/sbin:/usr/bin - - -pidfile=/var/run/soledad.pid \ -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.pem - -[ -r /etc/default/soledad ] && . /etc/default/soledad - -test -r /etc/leap/ || exit 0 - -. /lib/lsb/init-functions - - -case "$1" in - start) - echo -n "Starting soledad: twistd" - HOME="/var/lib/soledad/" \ - start-stop-daemon --start --quiet --exec /usr/local/bin/twistd -- \ - --pidfile=$pidfile \ - --logfile=$logfile \ - web \ - --wsgi=$obj \ - --https=$https_port \ - --certificate=$cert_path \ - --privkey=$privkey_path \ - --port=$plain_port - echo "." - ;; - - stop) - echo -n "Stopping soledad: twistd" - start-stop-daemon --stop --quiet \ - --pidfile $pidfile - echo "." - ;; - - restart) - $0 stop - $0 start - ;; - - force-reload) - $0 restart - ;; - - status) - status_of_proc -p $pidfile /usr/local/bin/twistd soledad && exit 0 || exit $? - ;; - - *) - echo "Usage: /etc/init.d/soledad {start|stop|restart|force-reload|status}" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/setup.py b/soledad/setup.py index b1c0b9db..747b02bd 100644 --- a/setup.py +++ b/soledad/setup.py @@ -16,7 +16,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os from setuptools import ( setup, find_packages @@ -24,20 +23,17 @@ from setuptools import ( install_requirements = [ - 'configparser', - 'couchdb', - 'leap.common', 'pysqlcipher', + 'pysqlite', # TODO: this should not be a dep, see #2945 'simplejson', - 'twisted>=12.0.0', # TODO: maybe we just want twisted-web? 'oauth', # this is not strictly needed by us, but we need it # until u1db adds it to its release as a dep. 'u1db', - 'requests', 'six==1.1.0', - 'pysqlite', 'scrypt', - 'routes', + 'pyxdg', + 'pycrypto', + 'pyOpenSSL', ] @@ -45,15 +41,11 @@ tests_requirements = [ 'mock', 'nose2', 'testscenarios', + 'leap.common', + 'leap.soledad_server', ] -if os.environ.get('VIRTUAL_ENV', None): - data_files = None -else: - # XXX this should go only for linux/mac - data_files = [("/etc/init.d/", ["pkg/soledad"])] - trove_classifiers = ( "Development Status :: 3 - Alpha", "Intended Audience :: Developers", @@ -70,7 +62,7 @@ trove_classifiers = ( setup( name='leap.soledad', - version='0.1.1', + version='0.2.2', url='https://leap.se/', license='GPLv3+', description='Synchronization of locally encrypted data among devices.', @@ -87,6 +79,6 @@ setup( test_suite='leap.soledad.tests', install_requires=install_requirements, tests_require=tests_requirements, - data_files=data_files, classifiers=trove_classifiers, + extras_require={'signaling': ['leap.common']}, ) diff --git a/src/leap/__init__.py b/soledad/src/leap/__init__.py index f48ad105..f48ad105 100644 --- a/src/leap/__init__.py +++ b/soledad/src/leap/__init__.py diff --git a/src/leap/soledad/__init__.py b/soledad/src/leap/soledad/__init__.py index fba275e3..956f47a7 100644 --- a/src/leap/soledad/__init__.py +++ b/soledad/src/leap/soledad/__init__.py @@ -36,6 +36,7 @@ import scrypt import httplib import socket import ssl +import errno from xdg import BaseDirectory @@ -47,16 +48,98 @@ from u1db.remote.ssl_match_hostname import ( # noqa ) -from leap.common import events -from leap.common.check import leap_assert -from leap.common.files import mkdir_p -from leap.soledad.backends import sqlcipher -from leap.soledad.backends.leap_backend import ( - LeapDocument, - LeapSyncTarget, -) +# +# 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 + + +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))) -from leap.soledad import shared_db +try: + from leap.common.check import leap_assert_type + soledad_assert_type = leap_assert_type +except ImportError: + pass + + +# +# 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...' +SOLEDAD_DONE_DOWNLOADING_KEYS = 'Done downloading keys.' +SOLEDAD_UPLOADING_KEYS = 'Uploading keys...' +SOLEDAD_DONE_UPLOADING_KEYS = 'Done uploading keys.' +SOLEDAD_NEW_DATA_TO_SYNC = 'New data available.' +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 + 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 + SOLEDAD_DONE_DOWNLOADING_KEYS = \ + events.events_pb2.SOLEDAD_DONE_DOWNLOADING_KEYS + SOLEDAD_UPLOADING_KEYS = events.events_pb2.SOLEDAD_UPLOADING_KEYS + SOLEDAD_DONE_UPLOADING_KEYS = \ + 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 + + +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 @@ -64,6 +147,10 @@ from leap.soledad.crypto import SoledadCrypto logger = logging.getLogger(name=__name__) +# +# Constants +# + SOLEDAD_CERT = None """ Path to the certificate file used to certify the SSL connection between @@ -77,6 +164,20 @@ SECRETS_DOC_ID_HASH_PREFIX = 'uuid-' # Soledad: local encrypted storage and remote encrypted sync. # +class NoStorageSecret(Exception): + """ + Raised when trying to use a storage secret but none is available. + """ + pass + + +class PassphraseTooShort(Exception): + """ + Raised when trying to change the passphrase but the provided passphrase is + too short. + """ + + class Soledad(object): """ Soledad provides encrypted data storage and sync. @@ -145,6 +246,12 @@ class Soledad(object): encryption. """ + MINIMUM_PASSPHRASE_LENGTH = 6 + """ + The minimum length for a passphrase. The passphrase length is only checked + when the user changes her passphras, not when she instantiates Soledad. + """ + IV_SEPARATOR = ":" """ A separator used for storing the encryption initial value prepended to the @@ -159,6 +266,8 @@ class Soledad(object): KDF_KEY = 'kdf' KDF_SALT_KEY = 'kdf_salt' KDF_LENGTH_KEY = 'kdf_length' + KDF_SCRYPT = 'scrypt' + CIPHER_AES256 = 'aes256' """ Keys used to access storage secrets in recovery documents. """ @@ -226,7 +335,7 @@ class Soledad(object): self.DEFAULT_PREFIX, self.LOCAL_DATABASE_FILE_NAME) # initialize server_url self._server_url = server_url - leap_assert( + soledad_assert( self._server_url is not None, 'Missing URL for Soledad server.') @@ -295,7 +404,13 @@ class Soledad(object): [self._local_db_path, self._secrets_path]) for path in paths: logger.info('Creating directory: %s.' % path) - mkdir_p(path) + try: + os.makedirs(path) + except OSError as exc: + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise def _init_db(self): """ @@ -324,11 +439,11 @@ 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( + self._db = sqlcipher_open( self._local_db_path, binascii.b2a_hex(key), # sqlcipher only accepts the hex version create=True, - document_factory=LeapDocument, + document_factory=SoledadDocument, crypto=self._crypto, raw_key=True) @@ -338,7 +453,7 @@ class Soledad(object): """ if hasattr(self, '_db') and isinstance( self._db, - sqlcipher.SQLCipherDatabase): + SQLCipherDatabase): self._db.close() def __del__(self): @@ -439,8 +554,8 @@ class Soledad(object): This method emits the following signals: - * leap.common.events.events_pb2.SOLEDAD_CREATING_KEYS - * leap.common.events.events_pb2.SOLEDAD_DONE_CREATING_KEYS + * SOLEDAD_CREATING_KEYS + * SOLEDAD_DONE_CREATING_KEYS A secret has the following structure: @@ -458,7 +573,7 @@ class Soledad(object): @return: The id of the generated secret. @rtype: str """ - events.signal(events.events_pb2.SOLEDAD_CREATING_KEYS, self._uuid) + signal(SOLEDAD_CREATING_KEYS, self._uuid) # generate random secret secret = os.urandom(self.GENERATED_SECRET_LENGTH) secret_id = sha256(secret).hexdigest() @@ -470,17 +585,16 @@ class Soledad(object): self._secrets[secret_id] = { # leap.soledad.crypto submodule uses AES256 for symmetric # encryption. - self.KDF_KEY: 'scrypt', # TODO: remove hard coded kdf + self.KDF_KEY: self.KDF_SCRYPT, self.KDF_SALT_KEY: binascii.b2a_base64(salt), self.KDF_LENGTH_KEY: len(key), - self.CIPHER_KEY: 'aes256', # TODO: remove hard coded cipher + self.CIPHER_KEY: self.CIPHER_AES256, self.LENGTH_KEY: len(secret), self.SECRET_KEY: '%s%s%s' % ( str(iv), self.IV_SEPARATOR, binascii.b2a_base64(ciphertext)), } self._store_secrets() - events.signal( - events.events_pb2.SOLEDAD_DONE_CREATING_KEYS, self._uuid) + signal(SOLEDAD_DONE_CREATING_KEYS, self._uuid) return secret_id def _store_secrets(self): @@ -508,6 +622,45 @@ class Soledad(object): with open(self._secrets_path, 'w') as f: f.write(json.dumps(data)) + def change_passphrase(self, new_passphrase): + """ + Change the passphrase that encrypts the storage secret. + + @param new_passphrase: The new passphrase. + @type new_passphrase: str + + @raise NoStorageSecret: Raised if there's no storage secret available. + """ + # maybe we want to add more checks to guarantee passphrase is + # reasonable? + soledad_assert_type(new_passphrase, str) + if len(new_passphrase) < self.MINIMUM_PASSPHRASE_LENGTH: + raise PassphraseTooShort( + 'Passphrase must be at least %d characters long!' % + self.MINIMUM_PASSPHRASE_LENGTH) + # ensure there's a secret for which the passphrase will be changed. + if not self._has_secret(): + raise NoStorageSecret() + secret = self._get_storage_secret() + # generate random salt + new_salt = os.urandom(self.SALT_LENGTH) + # get a 256-bit key + key = scrypt.hash(new_passphrase, new_salt, buflen=32) + iv, ciphertext = self._crypto.encrypt_sym(secret, key) + self._secrets[self._secret_id] = { + # leap.soledad.crypto submodule uses AES256 for symmetric + # encryption. + self.KDF_KEY: self.KDF_SCRYPT, # TODO: remove hard coded kdf + self.KDF_SALT_KEY: binascii.b2a_base64(new_salt), + self.KDF_LENGTH_KEY: len(key), + self.CIPHER_KEY: self.CIPHER_AES256, + self.LENGTH_KEY: len(secret), + self.SECRET_KEY: '%s%s%s' % ( + str(iv), self.IV_SEPARATOR, binascii.b2a_base64(ciphertext)), + } + self._store_secrets() + self._passphrase = new_passphrase + # # General crypto utility methods. # @@ -541,17 +694,15 @@ class Soledad(object): database. @return: a document with encrypted key material in its contents - @rtype: LeapDocument + @rtype: SoledadDocument """ - events.signal( - events.events_pb2.SOLEDAD_DOWNLOADING_KEYS, self._uuid) + signal(SOLEDAD_DOWNLOADING_KEYS, self._uuid) db = self._shared_db() if not db: logger.warning('No shared db found') return doc = db.get_doc(self._uuid_hash()) - events.signal( - events.events_pb2.SOLEDAD_DONE_DOWNLOADING_KEYS, self._uuid) + signal(SOLEDAD_DONE_DOWNLOADING_KEYS, self._uuid) return doc def _put_secrets_in_shared_db(self): @@ -563,26 +714,24 @@ class Soledad(object): Otherwise, upload keys to shared recovery database. """ - leap_assert( + soledad_assert( self._has_secret(), 'Tried to send keys to server but they don\'t exist in local ' 'storage.') # try to get secrets doc from server, otherwise create it doc = self._get_secrets_from_shared_db() if doc is None: - doc = LeapDocument(doc_id=self._uuid_hash()) + doc = SoledadDocument(doc_id=self._uuid_hash()) # fill doc with encrypted secrets doc.content = self.export_recovery_document(include_uuid=False) # upload secrets to server - events.signal( - events.events_pb2.SOLEDAD_UPLOADING_KEYS, self._uuid) + signal(SOLEDAD_UPLOADING_KEYS, self._uuid) db = self._shared_db() if not db: logger.warning('No shared db found') return db.put_doc(doc) - events.signal( - events.events_pb2.SOLEDAD_DONE_UPLOADING_KEYS, self._uuid) + signal(SOLEDAD_DONE_UPLOADING_KEYS, self._uuid) # # Document storage, retrieval and sync. @@ -593,7 +742,7 @@ class Soledad(object): Update a document in the local encrypted database. @param doc: the document to update - @type doc: LeapDocument + @type doc: SoledadDocument @return: the new revision identifier for the document @rtype: str @@ -605,7 +754,7 @@ class Soledad(object): Delete a document from the local encrypted database. @param doc: the document to delete - @type doc: LeapDocument + @type doc: SoledadDocument @return: the new revision identifier for the document @rtype: str @@ -624,7 +773,7 @@ class Soledad(object): @type include_deleted: bool @return: the document object or None - @rtype: LeapDocument + @rtype: SoledadDocument """ return self._db.get_doc(doc_id, include_deleted=include_deleted) @@ -669,7 +818,7 @@ class Soledad(object): @type doc_id: str @return: the new document - @rtype: LeapDocument + @rtype: SoledadDocument """ return self._db.create_doc(content, doc_id=doc_id) @@ -688,7 +837,7 @@ class Soledad(object): @param doc_id: An optional identifier specifying the document id. @type doc_id: @return: The new cocument - @rtype: LeapDocument + @rtype: SoledadDocument """ return self._db.create_doc_from_json(json, doc_id=doc_id) @@ -814,7 +963,7 @@ class Soledad(object): Mark a document as no longer conflicted. @param doc: a document with the new content to be inserted. - @type doc: LeapDocument + @type doc: SoledadDocument @param conflicted_doc_revs: a list of revisions that the new content supersedes. @type conflicted_doc_revs: list @@ -835,7 +984,7 @@ class Soledad(object): local_gen = self._db.sync( urlparse.urljoin(self.server_url, 'user-%s' % self._uuid), creds=self._creds, autocreate=True) - events.signal(events.events_pb2.SOLEDAD_DONE_DATA_SYNC, self._uuid) + signal(SOLEDAD_DONE_DATA_SYNC, self._uuid) return local_gen def need_sync(self, url): @@ -848,12 +997,11 @@ class Soledad(object): @return: Whether remote replica and local replica differ. @rtype: bool """ - target = LeapSyncTarget(url, creds=self._creds, crypto=self._crypto) + target = SoledadSyncTarget(url, creds=self._creds, crypto=self._crypto) info = target.get_sync_info(self._db._get_replica_uid()) # compare source generation with target's last known source generation if self._db._get_generation() != info[4]: - events.signal( - events.events_pb2.SOLEDAD_NEW_DATA_TO_SYNC, self._uuid) + signal(SOLEDAD_NEW_DATA_TO_SYNC, self._uuid) return True return False @@ -985,6 +1133,9 @@ class Soledad(object): # Monkey patching u1db to be able to provide a custom SSL cert #----------------------------------------------------------------------------- +# We need a more reasonable timeout (in seconds) +SOLEDAD_TIMEOUT = 10 + class VerifiedHTTPSConnection(httplib.HTTPSConnection): """HTTPSConnection verifying server side certificates.""" # derived from httplib.py @@ -992,7 +1143,7 @@ class VerifiedHTTPSConnection(httplib.HTTPSConnection): def connect(self): "Connect to a host on a given (SSL) port." sock = socket.create_connection((self.host, self.port), - self.timeout, self.source_address) + SOLEDAD_TIMEOUT, self.source_address) if self._tunnel_host: self.sock = sock self._tunnel() @@ -1005,3 +1156,6 @@ class VerifiedHTTPSConnection(httplib.HTTPSConnection): old__VerifiedHTTPSConnection = http_client._VerifiedHTTPSConnection http_client._VerifiedHTTPSConnection = VerifiedHTTPSConnection + + +__all__ = ['soledad_assert', 'Soledad'] diff --git a/src/leap/soledad/auth.py b/soledad/src/leap/soledad/auth.py index 8c093099..8c093099 100644 --- a/src/leap/soledad/auth.py +++ b/soledad/src/leap/soledad/auth.py diff --git a/soledad/src/leap/soledad/backends/__init__.py b/soledad/src/leap/soledad/backends/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/soledad/src/leap/soledad/backends/__init__.py diff --git a/soledad/src/leap/soledad/backends/leap_backend.py b/soledad/src/leap/soledad/backends/leap_backend.py new file mode 100644 index 00000000..a8d76b67 --- /dev/null +++ b/soledad/src/leap/soledad/backends/leap_backend.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# leap_backend.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/>. + + +""" +This file exists to provide backwards compatibility with code that uses +Soledad before the refactor that removed the leap_backend module. +""" + + +import logging +import warnings + + +from leap.soledad import document +from leap.soledad import target + + +logger = logging.getLogger(name=__name__) + + +def warn(oldclass, newclass): + """ + Warns about deprecation of C{oldclass}, which must be substituted by + C{newclass}. + + @param oldclass: The class that is deprecated. + @type oldclass: type + @param newclass: The class that should be used instead. + @type newclass: type + """ + message = \ + "%s is deprecated and will be removed soon. Please use %s instead." \ + % (str(oldclass), str(newclass)) + print message + logger.warning(message) + warnings.warn(message, DeprecationWarning, stacklevel=2) + + +class LeapDocument(document.SoledadDocument): + """ + This class exists to provide backwards compatibility with code that still + uses C{leap.soledad.backends.leap_backend.LeapDocument}. + """ + + def __init__(self, *args, **kwargs): + warn(self.__class__, document.SoledadDocument) + document.SoledadDocument.__init__(self, *args, **kwargs) + + +class EncryptionSchemes(target.EncryptionSchemes): + """ + This class exists to provide backwards compatibility with code that still + uses C{leap.soledad.backends.leap_backend.EncryptionSchemes}. + """ + + def __init__(self, *args, **kwargs): + warn(self.__class__, target.EncryptionSchemes) + target.EncryptionSchemes.__init__(self, *args, **kwargs) diff --git a/src/leap/soledad/crypto.py b/soledad/src/leap/soledad/crypto.py index e020eee6..bfad66d1 100644 --- a/src/leap/soledad/crypto.py +++ b/soledad/src/leap/soledad/crypto.py @@ -21,11 +21,35 @@ Cryptographic utilities for Soledad. """ +import os +import binascii import hmac import hashlib -from leap.common import crypto +from Crypto.Cipher import AES +from Crypto.Util import Counter + + +from leap.soledad import ( + soledad_assert, + soledad_assert_type, +) + + +class EncryptionMethods(object): + """ + Representation of encryption methods that can be used. + """ + + AES_256_CTR = 'aes-256-ctr' + + +class UnknownEncryptionMethod(Exception): + """ + Raised when trying to encrypt/decrypt with unknown method. + """ + pass class NoSymmetricSecret(Exception): @@ -51,7 +75,7 @@ class SoledadCrypto(object): self._soledad = soledad def encrypt_sym(self, data, key, - method=crypto.EncryptionMethods.AES_256_CTR): + method=EncryptionMethods.AES_256_CTR): """ Encrypt C{data} using a {password}. @@ -67,10 +91,24 @@ class SoledadCrypto(object): @return: A tuple with the initial value and the encrypted data. @rtype: (long, str) """ - return crypto.encrypt_sym(data, key, method) + soledad_assert_type(key, str) + + # AES-256 in CTR mode + if method == EncryptionMethods.AES_256_CTR: + soledad_assert( + len(key) == 32, # 32 x 8 = 256 bits. + 'Wrong key size: %s bits (must be 256 bits long).' % + (len(key) * 8)) + iv = os.urandom(8) + ctr = Counter.new(64, prefix=iv) + cipher = AES.new(key=key, mode=AES.MODE_CTR, counter=ctr) + return binascii.b2a_base64(iv), cipher.encrypt(data) + + # raise if method is unknown + raise UnknownEncryptionMethod('Unkwnown method: %s' % method) def decrypt_sym(self, data, key, - method=crypto.EncryptionMethods.AES_256_CTR, **kwargs): + method=EncryptionMethods.AES_256_CTR, **kwargs): """ Decrypt data using symmetric secret. @@ -88,7 +126,23 @@ class SoledadCrypto(object): @return: The decrypted data. @rtype: str """ - return crypto.decrypt_sym(data, key, method, **kwargs) + soledad_assert_type(key, str) + + # AES-256 in CTR mode + if method == EncryptionMethods.AES_256_CTR: + # assert params + soledad_assert( + len(key) == 32, # 32 x 8 = 256 bits. + 'Wrong key size: %s (must be 256 bits long).' % len(key)) + soledad_assert( + 'iv' in kwargs, + 'AES-256-CTR needs an initial value.') + ctr = Counter.new(64, prefix=binascii.a2b_base64(kwargs['iv'])) + cipher = AES.new(key=key, mode=AES.MODE_CTR, counter=ctr) + return cipher.decrypt(data) + + # raise if method is unknown + raise UnknownEncryptionMethod('Unkwnown method: %s' % method) def doc_passphrase(self, doc_id): """ diff --git a/soledad/src/leap/soledad/document.py b/soledad/src/leap/soledad/document.py new file mode 100644 index 00000000..cc24b53a --- /dev/null +++ b/soledad/src/leap/soledad/document.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# document.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 Soledad Document is an u1db.Document with lasers. +""" + + +from u1db import Document + + +# +# SoledadDocument +# + +class SoledadDocument(Document): + """ + Encryptable and syncable document. + + LEAP Documents can be flagged as syncable or not, so the replicas + might not sync every document. + """ + + def __init__(self, doc_id=None, rev=None, json='{}', has_conflicts=False, + syncable=True): + """ + Container for handling an encryptable document. + + @param doc_id: The unique document identifier. + @type doc_id: str + @param rev: The revision identifier of the document. + @type rev: str + @param json: The JSON string for this document. + @type json: str + @param has_conflicts: Boolean indicating if this document has conflicts + @type has_conflicts: bool + @param syncable: Should this document be synced with remote replicas? + @type syncable: bool + """ + Document.__init__(self, doc_id, rev, json, has_conflicts) + self._syncable = syncable + + def _get_syncable(self): + """ + Return whether this document is syncable. + + @return: Is this document syncable? + @rtype: bool + """ + return self._syncable + + def _set_syncable(self, syncable=True): + """ + Determine if this document should be synced with remote replicas. + + @param syncable: Should this document be synced with remote replicas? + @type syncable: bool + """ + self._syncable = syncable + + syncable = property( + _get_syncable, + _set_syncable, + doc="Determine if document should be synced with server." + ) + + def _get_rev(self): + """ + Get the document revision. + + Returning the revision as string solves the following exception in + Twisted web: + exceptions.TypeError: Can only pass-through bytes on Python 2 + + @return: The document revision. + @rtype: str + """ + if self._rev is None: + return None + return str(self._rev) + + def _set_rev(self, rev): + """ + Set document revision. + + @param rev: The new document revision. + @type rev: bytes + """ + self._rev = rev + + rev = property( + _get_rev, + _set_rev, + doc="Wrapper to ensure `doc.rev` is always returned as bytes.") + + diff --git a/src/leap/soledad/shared_db.py b/soledad/src/leap/soledad/shared_db.py index 33c5c484..33c5c484 100644 --- a/src/leap/soledad/shared_db.py +++ b/soledad/src/leap/soledad/shared_db.py diff --git a/src/leap/soledad/backends/sqlcipher.py b/soledad/src/leap/soledad/sqlcipher.py index d6d62f21..acbeabe6 100644 --- a/src/leap/soledad/backends/sqlcipher.py +++ b/soledad/src/leap/soledad/sqlcipher.py @@ -54,7 +54,7 @@ from pysqlcipher import dbapi2 from u1db import ( errors, ) -from leap.soledad.backends.leap_backend import LeapDocument +from leap.soledad.document import SoledadDocument # Monkey-patch u1db.backends.sqlite_backend with pysqlcipher.dbapi2 @@ -169,7 +169,7 @@ class SQLCipherDatabase(sqlite_backend.SQLitePartialExpandDatabase): def factory(doc_id=None, rev=None, json='{}', has_conflicts=False, syncable=True): - return LeapDocument(doc_id=doc_id, rev=rev, json=json, + return SoledadDocument(doc_id=doc_id, rev=rev, json=json, has_conflicts=has_conflicts, syncable=syncable) self.set_document_factory(factory) @@ -301,10 +301,10 @@ class SQLCipherDatabase(sqlite_backend.SQLitePartialExpandDatabase): @rtype: int """ from u1db.sync import Synchronizer - from leap.soledad.backends.leap_backend import LeapSyncTarget + from leap.soledad.target import SoledadSyncTarget return Synchronizer( self, - LeapSyncTarget(url, + SoledadSyncTarget(url, creds=creds, crypto=self._crypto)).sync(autocreate=autocreate) diff --git a/src/leap/soledad/backends/leap_backend.py b/soledad/src/leap/soledad/target.py index d92025db..8b7aa8c7 100644 --- a/src/leap/soledad/backends/leap_backend.py +++ b/soledad/src/leap/soledad/target.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# leap_backend.py +# target.py # Copyright (C) 2013 LEAP # # This program is free software: you can redistribute it and/or modify @@ -27,19 +27,17 @@ import hmac import binascii -from u1db import Document from u1db.remote import utils from u1db.errors import BrokenSyncStream from u1db.remote.http_target import HTTPSyncTarget -from leap.common.crypto import ( +from leap.soledad import soledad_assert +from leap.soledad.document import SoledadDocument +from leap.soledad.crypto import ( EncryptionMethods, UnknownEncryptionMethod, - encrypt_sym, - decrypt_sym, ) -from leap.common.check import leap_assert from leap.soledad.auth import TokenBasedAuth @@ -98,7 +96,7 @@ class MacMethods(object): # -# Crypto utilities for a LeapDocument. +# Crypto utilities for a SoledadDocument. # ENC_JSON_KEY = '_enc_json' @@ -161,15 +159,15 @@ def encrypt_doc(crypto, doc): @param crypto: A SoledadCryto instance used to perform the encryption. @type crypto: leap.soledad.crypto.SoledadCrypto @param doc: The document with contents to be encrypted. - @type doc: LeapDocument + @type doc: SoledadDocument @return: The JSON serialization of the dict representing the encrypted content. @rtype: str """ - leap_assert(doc.is_tombstone() is False) + soledad_assert(doc.is_tombstone() is False) # encrypt content using AES-256 CTR mode - iv, ciphertext = encrypt_sym( + iv, ciphertext = crypto.encrypt_sym( doc.get_json(), crypto.doc_passphrase(doc.doc_id), method=EncryptionMethods.AES_256_CTR) @@ -215,17 +213,17 @@ def decrypt_doc(crypto, doc): @param crypto: A SoledadCryto instance to perform the encryption. @type crypto: leap.soledad.crypto.SoledadCrypto @param doc: The document to be decrypted. - @type doc: LeapDocument + @type doc: SoledadDocument @return: The JSON serialization of the decrypted content. @rtype: str """ - leap_assert(doc.is_tombstone() is False) - leap_assert(ENC_JSON_KEY in doc.content) - leap_assert(ENC_SCHEME_KEY in doc.content) - leap_assert(ENC_METHOD_KEY in doc.content) - leap_assert(MAC_KEY in doc.content) - leap_assert(MAC_METHOD_KEY in doc.content) + soledad_assert(doc.is_tombstone() is False) + soledad_assert(ENC_JSON_KEY in doc.content) + soledad_assert(ENC_SCHEME_KEY in doc.content) + soledad_assert(ENC_METHOD_KEY in doc.content) + soledad_assert(MAC_KEY in doc.content) + soledad_assert(MAC_METHOD_KEY in doc.content) # verify MAC ciphertext = binascii.a2b_hex( # content is stored as hex. doc.content[ENC_JSON_KEY]) @@ -241,8 +239,8 @@ def decrypt_doc(crypto, doc): if enc_scheme == EncryptionSchemes.SYMKEY: enc_method = doc.content[ENC_METHOD_KEY] if enc_method == EncryptionMethods.AES_256_CTR: - leap_assert(ENC_IV_KEY in doc.content) - plainjson = decrypt_sym( + soledad_assert(ENC_IV_KEY in doc.content) + plainjson = crypto.decrypt_sym( ciphertext, crypto.doc_passphrase(doc.doc_id), method=enc_method, @@ -254,92 +252,11 @@ def decrypt_doc(crypto, doc): return plainjson -class LeapDocument(Document): - """ - Encryptable and syncable document. - - LEAP Documents can be flagged as syncable or not, so the replicas - might not sync every document. - """ - - def __init__(self, doc_id=None, rev=None, json='{}', has_conflicts=False, - syncable=True): - """ - Container for handling an encryptable document. - - @param doc_id: The unique document identifier. - @type doc_id: str - @param rev: The revision identifier of the document. - @type rev: str - @param json: The JSON string for this document. - @type json: str - @param has_conflicts: Boolean indicating if this document has conflicts - @type has_conflicts: bool - @param syncable: Should this document be synced with remote replicas? - @type syncable: bool - """ - Document.__init__(self, doc_id, rev, json, has_conflicts) - self._syncable = syncable - - def _get_syncable(self): - """ - Return whether this document is syncable. - - @return: Is this document syncable? - @rtype: bool - """ - return self._syncable - - def _set_syncable(self, syncable=True): - """ - Determine if this document should be synced with remote replicas. - - @param syncable: Should this document be synced with remote replicas? - @type syncable: bool - """ - self._syncable = syncable - - syncable = property( - _get_syncable, - _set_syncable, - doc="Determine if document should be synced with server." - ) - - def _get_rev(self): - """ - Get the document revision. - - Returning the revision as string solves the following exception in - Twisted web: - exceptions.TypeError: Can only pass-through bytes on Python 2 - - @return: The document revision. - @rtype: str - """ - if self._rev is None: - return None - return str(self._rev) - - def _set_rev(self, rev): - """ - Set document revision. - - @param rev: The new document revision. - @type rev: bytes - """ - self._rev = rev - - rev = property( - _get_rev, - _set_rev, - doc="Wrapper to ensure `doc.rev` is always returned as bytes.") - - # -# LeapSyncTarget +# SoledadSyncTarget # -class LeapSyncTarget(HTTPSyncTarget, TokenBasedAuth): +class SoledadSyncTarget(HTTPSyncTarget, TokenBasedAuth): """ A SyncTarget that encrypts data before sending and decrypts data after receiving. @@ -382,11 +299,11 @@ class LeapSyncTarget(HTTPSyncTarget, TokenBasedAuth): @staticmethod def connect(url, crypto=None): - return LeapSyncTarget(url, crypto=crypto) + return SoledadSyncTarget(url, crypto=crypto) def __init__(self, url, creds=None, crypto=None): """ - Initialize the LeapSyncTarget. + Initialize the SoledadSyncTarget. @param url: The url of the target replica to sync with. @type url: str @@ -443,7 +360,7 @@ class LeapSyncTarget(HTTPSyncTarget, TokenBasedAuth): #------------------------------------------------------------- # if arriving content was symmetrically encrypted, we decrypt # it. - doc = LeapDocument(entry['id'], entry['rev'], entry['content']) + doc = SoledadDocument(entry['id'], entry['rev'], entry['content']) if doc.content and ENC_SCHEME_KEY in doc.content: if doc.content[ENC_SCHEME_KEY] == \ EncryptionSchemes.SYMKEY: @@ -519,7 +436,7 @@ class LeapSyncTarget(HTTPSyncTarget, TokenBasedAuth): comma = ',' for doc, gen, trans_id in docs_by_generations: # skip non-syncable docs - if isinstance(doc, LeapDocument) and not doc.syncable: + if isinstance(doc, SoledadDocument) and not doc.syncable: continue #------------------------------------------------------------- # symmetric encryption of document's contents diff --git a/src/leap/soledad/tests/__init__.py b/soledad/src/leap/soledad/tests/__init__.py index 9fec5530..b33f866c 100644 --- a/src/leap/soledad/tests/__init__.py +++ b/soledad/src/leap/soledad/tests/__init__.py @@ -8,9 +8,9 @@ from mock import Mock from leap.soledad import Soledad +from leap.soledad.document import SoledadDocument from leap.soledad.crypto import SoledadCrypto -from leap.soledad.backends.leap_backend import ( - LeapDocument, +from leap.soledad.target import ( decrypt_doc, ENC_SCHEME_KEY, ) @@ -37,9 +37,9 @@ class BaseSoledadTest(BaseLeapTest): self.email = ADDRESS # open test dbs self._db1 = u1db.open(self.db1_file, create=True, - document_factory=LeapDocument) + document_factory=SoledadDocument) self._db2 = u1db.open(self.db2_file, create=True, - document_factory=LeapDocument) + document_factory=SoledadDocument) # initialize soledad by hand so we can control keys self._soledad = self._soledad_instance(user=self.email) @@ -257,11 +257,3 @@ RZXoH+FTg9UAW87eqU610npOkT6cRaBxaMK/mDtGNdc= =JTFu -----END PGP PRIVATE KEY BLOCK----- """ - -__all__ = [ - 'test_couch', - 'test_encrypted', - 'test_leap_backend', - 'test_sqlcipher', - 'u1db_tests', -] diff --git a/src/leap/soledad/tests/couchdb.ini.template b/soledad/src/leap/soledad/tests/couchdb.ini.template index 7d0316f0..7d0316f0 100644 --- a/src/leap/soledad/tests/couchdb.ini.template +++ b/soledad/src/leap/soledad/tests/couchdb.ini.template diff --git a/src/leap/soledad/tests/test_couch.py b/soledad/src/leap/soledad/tests/test_couch.py index 60a61b71..a84bb46c 100644 --- a/src/leap/soledad/tests/test_couch.py +++ b/soledad/src/leap/soledad/tests/test_couch.py @@ -27,14 +27,12 @@ from base64 import b64decode from leap.common.files import mkdir_p -from leap.soledad.backends import couch +from leap.soledad_server import couch from leap.soledad.tests import u1db_tests as tests from leap.soledad.tests.u1db_tests import test_backends from leap.soledad.tests.u1db_tests import test_sync import simplejson as json -from leap.soledad.backends.leap_backend import ( - LeapDocument, -) +from leap.soledad.document import SoledadDocument #----------------------------------------------------------------------------- @@ -185,7 +183,7 @@ def copy_couch_database_for_test(test, db): def make_document_for_test(test, doc_id, rev, content, has_conflicts=False): - return LeapDocument(doc_id, rev, content, has_conflicts=has_conflicts) + return SoledadDocument(doc_id, rev, content, has_conflicts=has_conflicts) COUCH_SCENARIOS = [ @@ -318,7 +316,6 @@ class CouchDatabaseSyncTests(test_sync.DatabaseSyncTests, CouchDBTestCase): self.db2 = None self.db3 = None test_sync.DatabaseSyncTests.setUp(self) - CouchDBTestCase.setUp(self) def tearDown(self): self.db and self.db.delete_database() diff --git a/src/leap/soledad/tests/test_crypto.py b/soledad/src/leap/soledad/tests/test_crypto.py index ae84dad3..c727a2ff 100644 --- a/src/leap/soledad/tests/test_crypto.py +++ b/soledad/src/leap/soledad/tests/test_crypto.py @@ -25,27 +25,22 @@ import shutil import tempfile import simplejson as json import hashlib +import binascii -from leap.soledad.backends.leap_backend import ( - LeapDocument, - encrypt_doc, - decrypt_doc, - EncryptionSchemes, - LeapSyncTarget, - ENC_JSON_KEY, - ENC_SCHEME_KEY, - MAC_METHOD_KEY, - MAC_KEY, - UnknownMacMethod, - WrongMac, +from leap.common.testing.basetest import BaseLeapTest +from Crypto import Random + + +from leap.common.testing.basetest import BaseLeapTest +from leap.soledad import ( + Soledad, + crypto, + target, ) -from leap.soledad.backends.couch import CouchDatabase -from leap.soledad import Soledad -from leap.soledad.crypto import SoledadCrypto -from leap.soledad.tests import BaseSoledadTest -from leap.soledad.tests.test_couch import CouchDBTestCase +from leap.soledad.document import SoledadDocument from leap.soledad.tests import ( + BaseSoledadTest, KEY_FINGERPRINT, PRIVATE_KEY, ) @@ -54,8 +49,6 @@ from leap.soledad.tests.u1db_tests import ( nested_doc, TestCaseWithServer, ) -from leap.soledad.tests.test_leap_backend import make_leap_document_for_test -from leap.soledad.backends.couch import CouchServerState class EncryptedSyncTestCase(BaseSoledadTest): @@ -68,112 +61,22 @@ class EncryptedSyncTestCase(BaseSoledadTest): Test encrypting and decrypting documents. """ simpledoc = {'key': 'val'} - doc1 = LeapDocument(doc_id='id') + doc1 = SoledadDocument(doc_id='id') doc1.content = simpledoc # encrypt doc - doc1.set_json(encrypt_doc(self._soledad._crypto, doc1)) + doc1.set_json(target.encrypt_doc(self._soledad._crypto, doc1)) # assert content is different and includes keys self.assertNotEqual( simpledoc, doc1.content, 'incorrect document encryption') - self.assertTrue(ENC_JSON_KEY in doc1.content) - self.assertTrue(ENC_SCHEME_KEY in doc1.content) + self.assertTrue(target.ENC_JSON_KEY in doc1.content) + self.assertTrue(target.ENC_SCHEME_KEY in doc1.content) # decrypt doc - doc1.set_json(decrypt_doc(self._soledad._crypto, doc1)) + doc1.set_json(target.decrypt_doc(self._soledad._crypto, doc1)) self.assertEqual( simpledoc, doc1.content, 'incorrect document encryption') -#from leap.soledad.server import SoledadApp, SoledadAuthMiddleware -# -# -#def make_token_leap_app(test, state): -# app = SoledadApp(state) -# application = SoledadAuthMiddleware(app, prefix='/soledad/') -# return application -# -# -#def leap_sync_target(test, path): -# return LeapSyncTarget(test.getURL(path)) -# -# -#def token_leap_sync_target(test, path): -# st = leap_sync_target(test, 'soledad/' + path) -# st.set_token_credentials('any_user', 'any_token') -# return st -# -# -#class EncryptedCouchSyncTest(CouchDBTestCase, TestCaseWithServer): -# -# make_app_with_state = make_token_leap_app -# -# make_document_for_test = make_leap_document_for_test -# -# sync_target = token_leap_sync_target -# -# def make_app(self): -# # potential hook point -# self.request_state = CouchServerState(self._couch_url) -# return self.make_app_with_state(self.request_state) -# -# def _soledad_instance(self, user='leap@leap.se', prefix='', -# bootstrap=False, gnupg_home='/gnupg', -# secrets_path='/secret.gpg', -# local_db_path='/soledad.u1db'): -# return Soledad( -# user, -# '123', -# gnupg_home=self.tempdir+prefix+gnupg_home, -# secrets_path=self.tempdir+prefix+secrets_path, -# local_db_path=self.tempdir+prefix+local_db_path, -# bootstrap=bootstrap) -# -# def setUp(self): -# CouchDBTestCase.setUp(self) -# TestCaseWithServer.setUp(self) -# self.tempdir = tempfile.mkdtemp(suffix='.couch.test') -# # initialize soledad by hand so we can control keys -# self._soledad = self._soledad_instance('leap@leap.se') -# self._soledad._init_dirs() -# self._soledad._crypto = SoledadCrypto(self._soledad) -# if not self._soledad._has_get_storage_secret()(): -# self._soledad._gen_get_storage_secret()() -# self._soledad._load_get_storage_secret()() -# self._soledad._init_db() -# -# def tearDown(self): -# shutil.rmtree(self.tempdir) -# -# def test_encrypted_sym_sync(self): -# # get direct access to couchdb -# import ipdb; ipdb.set_trace() -# self._couch_url = 'http://localhost:' + str(self.wrapper.port) -# db = CouchDatabase(self._couch_url, 'testdb') -# # create and encrypt a doc to insert directly in couchdb -# doc = LeapDocument('doc-id') -# doc.set_json( -# encrypt_doc( -# self._soledad._crypto, 'doc-id', json.dumps(simple_doc))) -# db.put_doc(doc) -# # setup credentials for access to soledad server -# creds = { -# 'token': { -# 'uuid': 'leap@leap.se', -# 'token': '1234', -# } -# } -# # sync local soledad db with server -# self.assertTrue(self._soledad.get_doc('doc-id') is None) -# self.startServer() -# # TODO fix sync for test. -# #self._soledad.sync(self.getURL('soledad/testdb'), creds) -# # get and check doc -# doc = self._soledad.get_doc('doc-id') -# # TODO: fix below. -# #self.assertTrue(doc is not None) -# #self.assertTrue(doc.content == simple_doc) - - class RecoveryDocumentTestCase(BaseSoledadTest): def test_export_recovery_document_raw(self): @@ -199,7 +102,7 @@ class RecoveryDocumentTestCase(BaseSoledadTest): 'Failed settinng secret for symmetric encryption.') -class CryptoMethodsTestCase(BaseSoledadTest): +class SoledadSecretsTestCase(BaseSoledadTest): def test__gen_secret(self): # instantiate and save secret_id @@ -254,33 +157,85 @@ class MacAuthTestCase(BaseSoledadTest): Trying to decrypt a document with wrong MAC should raise. """ simpledoc = {'key': 'val'} - doc = LeapDocument(doc_id='id') + doc = SoledadDocument(doc_id='id') doc.content = simpledoc # encrypt doc - doc.set_json(encrypt_doc(self._soledad._crypto, doc)) - self.assertTrue(MAC_KEY in doc.content) - self.assertTrue(MAC_METHOD_KEY in doc.content) + doc.set_json(target.encrypt_doc(self._soledad._crypto, doc)) + self.assertTrue(target.MAC_KEY in doc.content) + self.assertTrue(target.MAC_METHOD_KEY in doc.content) # mess with MAC - doc.content[MAC_KEY] = '1234567890ABCDEF' + doc.content[target.MAC_KEY] = '1234567890ABCDEF' # try to decrypt doc self.assertRaises( - WrongMac, - decrypt_doc, self._soledad._crypto, doc) + target.WrongMac, + target.decrypt_doc, self._soledad._crypto, doc) def test_decrypt_with_unknown_mac_method_raises(self): """ Trying to decrypt a document with unknown MAC method should raise. """ simpledoc = {'key': 'val'} - doc = LeapDocument(doc_id='id') + doc = SoledadDocument(doc_id='id') doc.content = simpledoc # encrypt doc - doc.set_json(encrypt_doc(self._soledad._crypto, doc)) - self.assertTrue(MAC_KEY in doc.content) - self.assertTrue(MAC_METHOD_KEY in doc.content) + doc.set_json(target.encrypt_doc(self._soledad._crypto, doc)) + self.assertTrue(target.MAC_KEY in doc.content) + self.assertTrue(target.MAC_METHOD_KEY in doc.content) # mess with MAC method - doc.content[MAC_METHOD_KEY] = 'mymac' + doc.content[target.MAC_METHOD_KEY] = 'mymac' # try to decrypt doc self.assertRaises( - UnknownMacMethod, - decrypt_doc, self._soledad._crypto, doc) + target.UnknownMacMethod, + target.decrypt_doc, self._soledad._crypto, doc) + + +class SoledadCryptoTestCase(BaseSoledadTest): + + def test_encrypt_decrypt_sym(self): + # generate 256-bit key + key = Random.new().read(32) + iv, cyphertext = self._soledad._crypto.encrypt_sym( + 'data', key, + method=crypto.EncryptionMethods.AES_256_CTR) + self.assertTrue(cyphertext is not None) + self.assertTrue(cyphertext != '') + self.assertTrue(cyphertext != 'data') + plaintext = self._soledad._crypto.decrypt_sym( + cyphertext, key, iv=iv, + method=crypto.EncryptionMethods.AES_256_CTR) + self.assertEqual('data', plaintext) + + def test_decrypt_with_wrong_iv_fails(self): + key = Random.new().read(32) + iv, cyphertext = self._soledad._crypto.encrypt_sym( + 'data', key, + method=crypto.EncryptionMethods.AES_256_CTR) + self.assertTrue(cyphertext is not None) + self.assertTrue(cyphertext != '') + self.assertTrue(cyphertext != 'data') + # get a different iv by changing the first byte + rawiv = binascii.a2b_base64(iv) + wrongiv = rawiv + while wrongiv == rawiv: + wrongiv = os.urandom(1) + rawiv[1:] + plaintext = self._soledad._crypto.decrypt_sym( + cyphertext, key, iv=binascii.b2a_base64(wrongiv), + method=crypto.EncryptionMethods.AES_256_CTR) + self.assertNotEqual('data', plaintext) + + def test_decrypt_with_wrong_key_fails(self): + key = Random.new().read(32) + iv, cyphertext = self._soledad._crypto.encrypt_sym( + 'data', key, + method=crypto.EncryptionMethods.AES_256_CTR) + self.assertTrue(cyphertext is not None) + self.assertTrue(cyphertext != '') + self.assertTrue(cyphertext != 'data') + wrongkey = Random.new().read(32) # 256-bits key + # ensure keys are different in case we are extremely lucky + while wrongkey == key: + wrongkey = Random.new().read(32) + plaintext = self._soledad._crypto.decrypt_sym( + cyphertext, wrongkey, iv=iv, + method=crypto.EncryptionMethods.AES_256_CTR) + self.assertNotEqual('data', plaintext) diff --git a/src/leap/soledad/tests/test_server.py b/soledad/src/leap/soledad/tests/test_server.py index ec3f636b..490d2fc8 100644 --- a/src/leap/soledad/tests/test_server.py +++ b/soledad/src/leap/soledad/tests/test_server.py @@ -25,15 +25,40 @@ import shutil import tempfile import simplejson as json import hashlib +import mock + + +from leap.soledad import Soledad +from leap.soledad_server import ( + SoledadApp, + SoledadAuthMiddleware, + URLToAuth, +) +from leap.soledad_server.couch import ( + CouchServerState, + CouchDatabase, +) +from leap.soledad import target -from leap.soledad.server import URLToAuth from leap.common.testing.basetest import BaseLeapTest +from leap.soledad.tests import ADDRESS +from leap.soledad.tests.u1db_tests import ( + TestCaseWithServer, + simple_doc, + nested_doc, +) +from leap.soledad.tests.test_couch import CouchDBTestCase +from leap.soledad.tests.test_target import ( + make_token_soledad_app, + make_leap_document_for_test, + token_leap_sync_target, +) -class SoledadServerTestCase(BaseLeapTest): +class ServerAuthorizationTestCase(BaseLeapTest): """ - Tests that guarantee that data will always be encrypted when syncing. + Tests related to Soledad server authorization. """ def setUp(self): @@ -237,3 +262,120 @@ class SoledadServerTestCase(BaseLeapTest): self.assertFalse( authmap.is_authorized( self._make_environ('/%s/sync-from/x' % dbname, 'POST'))) + + +class EncryptedSyncTestCase( + CouchDBTestCase, TestCaseWithServer): + """ + Tests for encrypted sync using Soledad server backed by a couch database. + """ + + @staticmethod + def make_app_with_state(state): + return make_token_soledad_app(state) + + make_document_for_test = make_leap_document_for_test + + sync_target = token_leap_sync_target + + def _soledad_instance(self, user='user-uuid', passphrase='123', + prefix='', + secrets_path=Soledad.STORAGE_SECRETS_FILE_NAME, + local_db_path='soledad.u1db', server_url='', + cert_file=None, auth_token=None, secret_id=None): + """ + Instantiate Soledad. + """ + + # this callback ensures we save a document which is sent to the shared + # db. + def _put_doc_side_effect(doc): + self._doc_put = doc + + # we need a mocked shared db or else Soledad will try to access the + # network to find if there are uploaded secrets. + class MockSharedDB(object): + + get_doc = mock.Mock(return_value=None) + put_doc = mock.Mock(side_effect=_put_doc_side_effect) + + def __call__(self): + return self + + Soledad._shared_db = MockSharedDB() + return Soledad( + user, + passphrase, + secrets_path=os.path.join(self.tempdir, prefix, secrets_path), + local_db_path=os.path.join( + self.tempdir, prefix, local_db_path), + server_url=server_url, + cert_file=cert_file, + auth_token=auth_token, + secret_id=secret_id) + + def make_app(self): + self.request_state = CouchServerState(self._couch_url) + return self.make_app_with_state(self.request_state) + + def setUp(self): + TestCaseWithServer.setUp(self) + CouchDBTestCase.setUp(self) + self.tempdir = tempfile.mkdtemp(prefix="leap_tests-") + self._couch_url = 'http://localhost:' + str(self.wrapper.port) + + def tearDown(self): + CouchDBTestCase.tearDown(self) + TestCaseWithServer.tearDown(self) + + def test_encrypted_sym_sync(self): + """ + Test the complete syncing chain between two soledad dbs using a + Soledad server backed by a couch database. + """ + self.startServer() + # instantiate soledad and create a document + sol1 = self._soledad_instance( + # token is verified in test_target.make_token_soledad_app + auth_token='auth-token' + ) + _, doclist = sol1.get_all_docs() + self.assertEqual([], doclist) + doc1 = sol1.create_doc(json.loads(simple_doc)) + # sync with server + sol1._server_url = self.getURL() + sol1.sync() + # assert doc was sent to couch db + db = CouchDatabase( + self._couch_url, + # the name of the user database is "user-<uuid>". + 'user-user-uuid', + ) + _, doclist = db.get_all_docs() + self.assertEqual(1, len(doclist)) + couchdoc = doclist[0] + # assert document structure in couch server + self.assertEqual(doc1.doc_id, couchdoc.doc_id) + self.assertEqual(doc1.rev, couchdoc.rev) + self.assertEqual(6, len(couchdoc.content)) + self.assertTrue(target.ENC_JSON_KEY in couchdoc.content) + self.assertTrue(target.ENC_SCHEME_KEY in couchdoc.content) + self.assertTrue(target.ENC_METHOD_KEY in couchdoc.content) + self.assertTrue(target.ENC_IV_KEY in couchdoc.content) + self.assertTrue(target.MAC_KEY in couchdoc.content) + self.assertTrue(target.MAC_METHOD_KEY in couchdoc.content) + # instantiate soledad with empty db, but with same secrets path + sol2 = self._soledad_instance(prefix='x', auth_token='auth-token') + _, doclist = sol2.get_all_docs() + self.assertEqual([], doclist) + sol2._secrets_path = sol1.secrets_path + sol2._load_secrets() + sol2._set_secret_id(sol1._secret_id) + # sync the new instance + sol2._server_url = self.getURL() + sol2.sync() + _, doclist = sol2.get_all_docs() + self.assertEqual(1, len(doclist)) + doc2 = doclist[0] + # assert incoming doc is equal to the first sent doc + self.assertEqual(doc1, doc2) diff --git a/src/leap/soledad/tests/test_soledad.py b/soledad/src/leap/soledad/tests/test_soledad.py index 09711f19..875ecc56 100644 --- a/src/leap/soledad/tests/test_soledad.py +++ b/soledad/src/leap/soledad/tests/test_soledad.py @@ -28,26 +28,19 @@ import simplejson as json from mock import Mock +from pysqlcipher.dbapi2 import DatabaseError from leap.common.testing.basetest import BaseLeapTest -from leap.common.events import ( - server, - component, - events_pb2 as proto, - register, - signal, -) +from leap.common.events import events_pb2 as proto from leap.soledad.tests import ( BaseSoledadTest, ADDRESS, ) from leap import soledad from leap.soledad import Soledad +from leap.soledad.document import SoledadDocument from leap.soledad.crypto import SoledadCrypto from leap.soledad.shared_db import SoledadSharedDatabase -from leap.soledad.backends.leap_backend import ( - LeapDocument, - LeapSyncTarget, -) +from leap.soledad.target import SoledadSyncTarget class AuxMethodsTestCase(BaseSoledadTest): @@ -69,7 +62,7 @@ class AuxMethodsTestCase(BaseSoledadTest): sol._gen_secret() sol._load_secrets() sol._init_db() - from leap.soledad.backends.sqlcipher import SQLCipherDatabase + from leap.soledad.sqlcipher import SQLCipherDatabase self.assertIsInstance(sol._db, SQLCipherDatabase) def test__init_config_defaults(self): @@ -114,6 +107,39 @@ class AuxMethodsTestCase(BaseSoledadTest): sol.local_db_path) self.assertEqual('value_1', sol.server_url) + def test_change_passphrase(self): + """ + Test if passphrase can be changed. + """ + sol = self._soledad_instance( + 'leap@leap.se', + passphrase='123') + doc = sol.create_doc({'simple': 'doc'}) + doc_id = doc.doc_id + # change the passphrase + sol.change_passphrase('654321') + # assert we can not use the old passphrase anymore + self.assertRaises( + DatabaseError, + self._soledad_instance, 'leap@leap.se', '123') + # use new passphrase and retrieve doc + sol2 = self._soledad_instance('leap@leap.se', '654321') + doc2 = sol2.get_doc(doc_id) + self.assertEqual(doc, doc2) + + def test_change_passphrase_with_short_passphrase_raises(self): + """ + Test if attempt to change passphrase passing a short passphrase + raises. + """ + sol = self._soledad_instance( + 'leap@leap.se', + passphrase='123') + # check that soledad complains about new passphrase length + self.assertRaises( + soledad.PassphraseTooShort, + sol.change_passphrase, '54321') + class SoledadSharedDBTestCase(BaseSoledadTest): """ @@ -123,7 +149,7 @@ class SoledadSharedDBTestCase(BaseSoledadTest): def setUp(self): BaseSoledadTest.setUp(self) self._shared_db = SoledadSharedDatabase( - 'https://provider/', LeapDocument, None) + 'https://provider/', SoledadDocument, None) def test__get_secrets_from_shared_db(self): """ @@ -165,7 +191,7 @@ class SoledadSignalingTestCase(BaseSoledadTest): def setUp(self): BaseSoledadTest.setUp(self) # mock signaling - soledad.events.signal = Mock() + soledad.signal = Mock() def tearDown(self): pass @@ -179,54 +205,54 @@ class SoledadSignalingTestCase(BaseSoledadTest): """ Test that a fresh soledad emits all bootstrap signals. """ - soledad.events.signal.reset_mock() + soledad.signal.reset_mock() # get a fresh instance so it emits all bootstrap signals sol = self._soledad_instance( secrets_path='alternative.json', local_db_path='alternative.u1db') # reverse call order so we can verify in the order the signals were # expected - soledad.events.signal.mock_calls.reverse() - soledad.events.signal.call_args = \ - soledad.events.signal.call_args_list[0] - soledad.events.signal.call_args_list.reverse() + soledad.signal.mock_calls.reverse() + soledad.signal.call_args = \ + soledad.signal.call_args_list[0] + soledad.signal.call_args_list.reverse() # assert signals - soledad.events.signal.assert_called_with( + soledad.signal.assert_called_with( proto.SOLEDAD_DOWNLOADING_KEYS, ADDRESS, ) - self._pop_mock_call(soledad.events.signal) - soledad.events.signal.assert_called_with( + self._pop_mock_call(soledad.signal) + soledad.signal.assert_called_with( proto.SOLEDAD_DONE_DOWNLOADING_KEYS, ADDRESS, ) - self._pop_mock_call(soledad.events.signal) - soledad.events.signal.assert_called_with( + self._pop_mock_call(soledad.signal) + soledad.signal.assert_called_with( proto.SOLEDAD_CREATING_KEYS, ADDRESS, ) - self._pop_mock_call(soledad.events.signal) - soledad.events.signal.assert_called_with( + self._pop_mock_call(soledad.signal) + soledad.signal.assert_called_with( proto.SOLEDAD_DONE_CREATING_KEYS, ADDRESS, ) - self._pop_mock_call(soledad.events.signal) - soledad.events.signal.assert_called_with( + self._pop_mock_call(soledad.signal) + soledad.signal.assert_called_with( proto.SOLEDAD_DOWNLOADING_KEYS, ADDRESS, ) - self._pop_mock_call(soledad.events.signal) - soledad.events.signal.assert_called_with( + self._pop_mock_call(soledad.signal) + soledad.signal.assert_called_with( proto.SOLEDAD_DONE_DOWNLOADING_KEYS, ADDRESS, ) - self._pop_mock_call(soledad.events.signal) - soledad.events.signal.assert_called_with( + self._pop_mock_call(soledad.signal) + soledad.signal.assert_called_with( proto.SOLEDAD_UPLOADING_KEYS, ADDRESS, ) - self._pop_mock_call(soledad.events.signal) - soledad.events.signal.assert_called_with( + self._pop_mock_call(soledad.signal) + soledad.signal.assert_called_with( proto.SOLEDAD_DONE_UPLOADING_KEYS, ADDRESS, ) @@ -235,32 +261,32 @@ class SoledadSignalingTestCase(BaseSoledadTest): """ Test that an existent soledad emits some of the bootstrap signals. """ - soledad.events.signal.reset_mock() + soledad.signal.reset_mock() # get an existent instance so it emits only some of bootstrap signals sol = self._soledad_instance() # reverse call order so we can verify in the order the signals were # expected - soledad.events.signal.mock_calls.reverse() - soledad.events.signal.call_args = \ - soledad.events.signal.call_args_list[0] - soledad.events.signal.call_args_list.reverse() + soledad.signal.mock_calls.reverse() + soledad.signal.call_args = \ + soledad.signal.call_args_list[0] + soledad.signal.call_args_list.reverse() # assert signals - soledad.events.signal.assert_called_with( + soledad.signal.assert_called_with( proto.SOLEDAD_DOWNLOADING_KEYS, ADDRESS, ) - self._pop_mock_call(soledad.events.signal) - soledad.events.signal.assert_called_with( + self._pop_mock_call(soledad.signal) + soledad.signal.assert_called_with( proto.SOLEDAD_DONE_DOWNLOADING_KEYS, ADDRESS, ) - self._pop_mock_call(soledad.events.signal) - soledad.events.signal.assert_called_with( + self._pop_mock_call(soledad.signal) + soledad.signal.assert_called_with( proto.SOLEDAD_UPLOADING_KEYS, ADDRESS, ) - self._pop_mock_call(soledad.events.signal) - soledad.events.signal.assert_called_with( + self._pop_mock_call(soledad.signal) + soledad.signal.assert_called_with( proto.SOLEDAD_DONE_UPLOADING_KEYS, ADDRESS, ) @@ -269,7 +295,7 @@ class SoledadSignalingTestCase(BaseSoledadTest): """ Test Soledad emits SOLEDAD_CREATING_KEYS signal. """ - soledad.events.signal.reset_mock() + soledad.signal.reset_mock() # get a fresh instance so it emits all bootstrap signals sol = self._soledad_instance() # mock the actual db sync so soledad does not try to connect to the @@ -278,7 +304,7 @@ class SoledadSignalingTestCase(BaseSoledadTest): # do the sync sol.sync() # assert the signal has been emitted - soledad.events.signal.assert_called_with( + soledad.signal.assert_called_with( proto.SOLEDAD_DONE_DATA_SYNC, ADDRESS, ) @@ -287,16 +313,18 @@ class SoledadSignalingTestCase(BaseSoledadTest): """ Test Soledad emits SOLEDAD_CREATING_KEYS signal. """ - soledad.events.signal.reset_mock() + soledad.signal.reset_mock() sol = self._soledad_instance() # mock the sync target - LeapSyncTarget.get_sync_info = Mock(return_value=[0, 0, 0, 0, 2]) + old_get_sync_info = SoledadSyncTarget.get_sync_info + SoledadSyncTarget.get_sync_info = Mock(return_value=[0, 0, 0, 0, 2]) # mock our generation so soledad thinks there's new data to sync sol._db._get_generation = Mock(return_value=1) # check for new data to sync sol.need_sync('http://provider/userdb') # assert the signal has been emitted - soledad.events.signal.assert_called_with( + soledad.signal.assert_called_with( proto.SOLEDAD_NEW_DATA_TO_SYNC, ADDRESS, ) + SoledadSyncTarget.get_sync_info = old_get_sync_info diff --git a/src/leap/soledad/tests/test_sqlcipher.py b/soledad/src/leap/soledad/tests/test_sqlcipher.py index 9741bd4e..25d04861 100644 --- a/src/leap/soledad/tests/test_sqlcipher.py +++ b/soledad/src/leap/soledad/tests/test_sqlcipher.py @@ -43,13 +43,13 @@ from u1db.backends.sqlite_backend import SQLitePartialExpandDatabase # soledad stuff. -from leap.soledad.backends.sqlcipher import ( +from leap.soledad.document import SoledadDocument +from leap.soledad.sqlcipher import ( SQLCipherDatabase, DatabaseIsNotEncrypted, + open as u1db_open, ) -from leap.soledad.backends.sqlcipher import open as u1db_open -from leap.soledad.backends.leap_backend import ( - LeapDocument, +from leap.soledad.target import ( EncryptionSchemes, decrypt_doc, ENC_JSON_KEY, @@ -63,7 +63,7 @@ from leap.soledad.tests.u1db_tests import test_sqlite_backend from leap.soledad.tests.u1db_tests import test_backends from leap.soledad.tests.u1db_tests import test_open from leap.soledad.tests.u1db_tests import test_sync -from leap.soledad.backends.leap_backend import LeapSyncTarget +from leap.soledad.target import SoledadSyncTarget from leap.common.testing.basetest import BaseLeapTest PASSWORD = '123456' @@ -115,7 +115,7 @@ def copy_sqlcipher_database_for_test(test, db): def make_document_for_test(test, doc_id, rev, content, has_conflicts=False): - return LeapDocument(doc_id, rev, content, has_conflicts=has_conflicts) + return SoledadDocument(doc_id, rev, content, has_conflicts=has_conflicts) SQLCIPHER_SCENARIOS = [ @@ -205,7 +205,7 @@ class TestSQLCipherDatabase(test_sqlite_backend.TestSQLiteDatabase): self.assertTrue(db2._is_initialized(db1._get_sqlite_handle().cursor())) -class TestAlternativeDocument(LeapDocument): +class TestAlternativeDocument(SoledadDocument): """A (not very) alternative implementation of Document.""" @@ -272,7 +272,7 @@ class TestSQLCipherPartialExpandDatabase( path, PASSWORD, document_factory=TestAlternativeDocument) doc = db2.create_doc({}) - self.assertTrue(isinstance(doc, LeapDocument)) + self.assertTrue(isinstance(doc, SoledadDocument)) def test__open_database_non_existent(self): temp_dir = self.createTempDir(prefix='u1db-test-') @@ -341,7 +341,7 @@ class TestSQLCipherPartialExpandDatabase( path, PASSWORD, create=False, document_factory=TestAlternativeDocument) doc = db2.create_doc({}) - self.assertTrue(isinstance(doc, LeapDocument)) + self.assertTrue(isinstance(doc, SoledadDocument)) def test_open_database_create(self): temp_dir = self.createTempDir(prefix='u1db-test-') @@ -401,7 +401,7 @@ class SQLCipherOpen(test_open.TestU1DBOpen): document_factory=TestAlternativeDocument) self.addCleanup(db.close) doc = db.create_doc({}) - self.assertTrue(isinstance(doc, LeapDocument)) + self.assertTrue(isinstance(doc, SoledadDocument)) def test_open_existing(self): db = SQLCipherDatabase(self.db_path, PASSWORD) @@ -438,7 +438,7 @@ def sync_via_synchronizer_and_leap(test, db_source, db_target, if trace_hook: test.skipTest("full trace hook unsupported over http") path = test._http_at[db_target] - target = LeapSyncTarget.connect(test.getURL(path), test._soledad._crypto) + target = SoledadSyncTarget.connect(test.getURL(path), test._soledad._crypto) target.set_token_credentials('user-uuid', 'auth-token') if trace_hook_shallow: target._set_trace_hook_shallow(trace_hook_shallow) @@ -662,7 +662,7 @@ class SQLCipherDatabaseSyncTests( def _make_local_db_and_leap_target(test, path='test'): test.startServer() db = test.request_state._create_database(os.path.basename(path)) - st = LeapSyncTarget.connect(test.getURL(path), test._soledad._crypto) + st = SoledadSyncTarget.connect(test.getURL(path), test._soledad._crypto) st.set_token_credentials('user-uuid', 'auth-token') return db, st @@ -791,7 +791,7 @@ class SQLCipherEncryptionTest(BaseLeapTest): # trying to open an encrypted database with the regular u1db # backend should raise a DatabaseError exception. SQLitePartialExpandDatabase(self.DB_FILE, - document_factory=LeapDocument) + document_factory=SoledadDocument) raise DatabaseIsNotEncrypted() except dbapi2.DatabaseError: # at this point we know that the regular U1DB sqlcipher backend @@ -807,7 +807,7 @@ class SQLCipherEncryptionTest(BaseLeapTest): SQLCipher backend should not succeed to open unencrypted databases. """ db = SQLitePartialExpandDatabase(self.DB_FILE, - document_factory=LeapDocument) + document_factory=SoledadDocument) db.create_doc_from_json(tests.simple_doc) db.close() try: diff --git a/src/leap/soledad/tests/test_leap_backend.py b/soledad/src/leap/soledad/tests/test_target.py index 6914e869..73c9fe68 100644 --- a/src/leap/soledad/tests/test_leap_backend.py +++ b/soledad/src/leap/soledad/tests/test_target.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# test_leap_backend.py +# test_target.py # Copyright (C) 2013 LEAP # # This program is free software: you can redistribute it and/or modify @@ -33,15 +33,17 @@ from u1db.remote import ( http_database, http_target, ) -from routes.mapper import Mapper from leap import soledad -from leap.soledad.backends import leap_backend -from leap.soledad.server import ( +from leap.soledad import ( + target, + auth, +) +from leap.soledad.document import SoledadDocument +from leap.soledad_server import ( SoledadApp, SoledadAuthMiddleware, ) -from leap.soledad import auth from leap.soledad.tests import u1db_tests as tests @@ -61,7 +63,7 @@ from leap.soledad.tests.u1db_tests import test_sync def make_leap_document_for_test(test, doc_id, rev, content, has_conflicts=False): - return leap_backend.LeapDocument( + return SoledadDocument( doc_id, rev, content, has_conflicts=has_conflicts) @@ -126,7 +128,7 @@ def copy_token_http_database_for_test(test, db): return http_db -class LeapTests(test_backends.AllDatabaseTests, BaseSoledadTest): +class SoledadTests(test_backends.AllDatabaseTests, BaseSoledadTest): scenarios = LEAP_SCENARIOS + [ ('token_http', {'make_database_for_test': @@ -143,7 +145,7 @@ class LeapTests(test_backends.AllDatabaseTests, BaseSoledadTest): # The following tests come from `u1db.tests.test_http_client`. #----------------------------------------------------------------------------- -class TestLeapClientBase(test_http_client.TestHTTPClientBase): +class TestSoledadClientBase(test_http_client.TestHTTPClientBase): """ This class should be used to test Token auth. """ @@ -230,13 +232,13 @@ class TestLeapClientBase(test_http_client.TestHTTPClientBase): # The following tests come from `u1db.tests.test_document`. #----------------------------------------------------------------------------- -class TestLeapDocument(test_document.TestDocument, BaseSoledadTest): +class TestSoledadDocument(test_document.TestDocument, BaseSoledadTest): scenarios = ([( 'leap', {'make_document_for_test': make_leap_document_for_test})]) -class TestLeapPyDocument(test_document.TestPyDocument, BaseSoledadTest): +class TestSoledadPyDocument(test_document.TestPyDocument, BaseSoledadTest): scenarios = ([( 'leap', {'make_document_for_test': make_leap_document_for_test})]) @@ -246,7 +248,7 @@ class TestLeapPyDocument(test_document.TestPyDocument, BaseSoledadTest): # The following tests come from `u1db.tests.test_remote_sync_target`. #----------------------------------------------------------------------------- -class TestLeapSyncTargetBasics( +class TestSoledadSyncTargetBasics( test_remote_sync_target.TestHTTPSyncTargetBasics): """ Some tests had to be copied to this class so we can instantiate our own @@ -254,14 +256,14 @@ class TestLeapSyncTargetBasics( """ def test_parse_url(self): - remote_target = leap_backend.LeapSyncTarget('http://127.0.0.1:12345/') + remote_target = target.SoledadSyncTarget('http://127.0.0.1:12345/') self.assertEqual('http', remote_target._url.scheme) self.assertEqual('127.0.0.1', remote_target._url.hostname) self.assertEqual(12345, remote_target._url.port) self.assertEqual('/', remote_target._url.path) -class TestLeapParsingSyncStream( +class TestSoledadParsingSyncStream( test_remote_sync_target.TestParsingSyncStream, BaseSoledadTest): """ @@ -281,10 +283,10 @@ class TestLeapParsingSyncStream( """ Test adapted to use encrypted content. """ - doc = leap_backend.LeapDocument('i', rev='r') + doc = SoledadDocument('i', rev='r') doc.content = {} - enc_json = leap_backend.encrypt_doc(self._soledad._crypto, doc) - tgt = leap_backend.LeapSyncTarget( + enc_json = target.encrypt_doc(self._soledad._crypto, doc) + tgt = target.SoledadSyncTarget( "http://foo/foo", crypto=self._soledad._crypto) self.assertRaises(u1db.errors.BrokenSyncStream, @@ -297,7 +299,7 @@ class TestLeapParsingSyncStream( lambda doc, gen, trans_id: None) def test_wrong_start(self): - tgt = leap_backend.LeapSyncTarget("http://foo/foo") + tgt = target.SoledadSyncTarget("http://foo/foo") self.assertRaises(u1db.errors.BrokenSyncStream, tgt._parse_sync_stream, "{}\r\n]", None) @@ -309,7 +311,7 @@ class TestLeapParsingSyncStream( tgt._parse_sync_stream, "", None) def test_wrong_end(self): - tgt = leap_backend.LeapSyncTarget("http://foo/foo") + tgt = target.SoledadSyncTarget("http://foo/foo") self.assertRaises(u1db.errors.BrokenSyncStream, tgt._parse_sync_stream, "[\r\n{}", None) @@ -318,7 +320,7 @@ class TestLeapParsingSyncStream( tgt._parse_sync_stream, "[\r\n", None) def test_missing_comma(self): - tgt = leap_backend.LeapSyncTarget("http://foo/foo") + tgt = target.SoledadSyncTarget("http://foo/foo") self.assertRaises(u1db.errors.BrokenSyncStream, tgt._parse_sync_stream, @@ -326,13 +328,13 @@ class TestLeapParsingSyncStream( '"content": "c", "gen": 3}\r\n]', None) def test_no_entries(self): - tgt = leap_backend.LeapSyncTarget("http://foo/foo") + tgt = target.SoledadSyncTarget("http://foo/foo") self.assertRaises(u1db.errors.BrokenSyncStream, tgt._parse_sync_stream, "[\r\n]", None) def test_error_in_stream(self): - tgt = leap_backend.LeapSyncTarget("http://foo/foo") + tgt = target.SoledadSyncTarget("http://foo/foo") self.assertRaises(u1db.errors.Unavailable, tgt._parse_sync_stream, @@ -353,7 +355,7 @@ class TestLeapParsingSyncStream( # def leap_sync_target(test, path): - return leap_backend.LeapSyncTarget( + return target.SoledadSyncTarget( test.getURL(path), crypto=test._soledad._crypto) @@ -363,7 +365,7 @@ def token_leap_sync_target(test, path): return st -class TestLeapSyncTarget( +class TestSoledadSyncTarget( test_remote_sync_target.TestRemoteSyncTargets, BaseSoledadTest): scenarios = [ @@ -497,14 +499,14 @@ class TestLeapSyncTarget( def token_leap_https_sync_target(test, host, path): _, port = test.server.server_address - st = leap_backend.LeapSyncTarget( + st = target.SoledadSyncTarget( 'https://%s:%d/%s' % (host, port, path), crypto=test._soledad._crypto) st.set_token_credentials('user-uuid', 'auth-token') return st -class TestLeapSyncTargetHttpsSupport(test_https.TestHttpSyncTargetHttpsSupport, +class TestSoledadSyncTargetHttpsSupport(test_https.TestHttpSyncTargetHttpsSupport, BaseSoledadTest): scenarios = [ @@ -598,7 +600,7 @@ class TestHTTPDatabaseWithCreds( def _make_local_db_and_leap_target(test, path='test'): test.startServer() db = test.request_state._create_database(os.path.basename(path)) - st = leap_backend.LeapSyncTarget.connect( + st = target.SoledadSyncTarget.connect( test.getURL(path), crypto=test._soledad._crypto) return db, st @@ -616,7 +618,7 @@ target_scenarios = [ ] -class LeapDatabaseSyncTargetTests( +class SoledadDatabaseSyncTargetTests( test_sync.DatabaseSyncTargetTests, BaseSoledadTest): scenarios = ( @@ -702,7 +704,7 @@ class LeapDatabaseSyncTargetTests( [(doc.doc_id, doc.rev), (doc2.doc_id, doc2.rev)]}) -class TestLeapDbSync(test_sync.TestDbSync, BaseSoledadTest): +class TestSoledadDbSync(test_sync.TestDbSync, BaseSoledadTest): """Test db.sync remote sync shortcut""" scenarios = [ @@ -722,7 +724,7 @@ class TestLeapDbSync(test_sync.TestDbSync, BaseSoledadTest): def do_sync(self, target_name): """ - Perform sync using LeapSyncTarget and Token auth. + Perform sync using SoledadSyncTarget and Token auth. """ if self.token: extra = dict(creds={'token': { @@ -732,7 +734,7 @@ class TestLeapDbSync(test_sync.TestDbSync, BaseSoledadTest): target_url = self.getURL(target_name) return Synchronizer( self.db, - leap_backend.LeapSyncTarget( + target.SoledadSyncTarget( target_url, crypto=self._soledad._crypto, **extra)).sync(autocreate=True) diff --git a/src/leap/soledad/tests/u1db_tests/README b/soledad/src/leap/soledad/tests/u1db_tests/README index 605f01fa..605f01fa 100644 --- a/src/leap/soledad/tests/u1db_tests/README +++ b/soledad/src/leap/soledad/tests/u1db_tests/README diff --git a/src/leap/soledad/tests/u1db_tests/__init__.py b/soledad/src/leap/soledad/tests/u1db_tests/__init__.py index 43304b43..43304b43 100644 --- a/src/leap/soledad/tests/u1db_tests/__init__.py +++ b/soledad/src/leap/soledad/tests/u1db_tests/__init__.py diff --git a/src/leap/soledad/tests/u1db_tests/test_backends.py b/soledad/src/leap/soledad/tests/u1db_tests/test_backends.py index a53b01ba..a53b01ba 100644 --- a/src/leap/soledad/tests/u1db_tests/test_backends.py +++ b/soledad/src/leap/soledad/tests/u1db_tests/test_backends.py diff --git a/src/leap/soledad/tests/u1db_tests/test_document.py b/soledad/src/leap/soledad/tests/u1db_tests/test_document.py index e706e1a9..e706e1a9 100644 --- a/src/leap/soledad/tests/u1db_tests/test_document.py +++ b/soledad/src/leap/soledad/tests/u1db_tests/test_document.py diff --git a/src/leap/soledad/tests/u1db_tests/test_http_app.py b/soledad/src/leap/soledad/tests/u1db_tests/test_http_app.py index e0729aa2..e0729aa2 100644 --- a/src/leap/soledad/tests/u1db_tests/test_http_app.py +++ b/soledad/src/leap/soledad/tests/u1db_tests/test_http_app.py diff --git a/src/leap/soledad/tests/u1db_tests/test_http_client.py b/soledad/src/leap/soledad/tests/u1db_tests/test_http_client.py index 42e98461..42e98461 100644 --- a/src/leap/soledad/tests/u1db_tests/test_http_client.py +++ b/soledad/src/leap/soledad/tests/u1db_tests/test_http_client.py diff --git a/src/leap/soledad/tests/u1db_tests/test_http_database.py b/soledad/src/leap/soledad/tests/u1db_tests/test_http_database.py index f21e6da1..f21e6da1 100644 --- a/src/leap/soledad/tests/u1db_tests/test_http_database.py +++ b/soledad/src/leap/soledad/tests/u1db_tests/test_http_database.py diff --git a/src/leap/soledad/tests/u1db_tests/test_https.py b/soledad/src/leap/soledad/tests/u1db_tests/test_https.py index 62180f8c..62180f8c 100644 --- a/src/leap/soledad/tests/u1db_tests/test_https.py +++ b/soledad/src/leap/soledad/tests/u1db_tests/test_https.py diff --git a/src/leap/soledad/tests/u1db_tests/test_open.py b/soledad/src/leap/soledad/tests/u1db_tests/test_open.py index 0ff307e8..0ff307e8 100644 --- a/src/leap/soledad/tests/u1db_tests/test_open.py +++ b/soledad/src/leap/soledad/tests/u1db_tests/test_open.py diff --git a/src/leap/soledad/tests/u1db_tests/test_remote_sync_target.py b/soledad/src/leap/soledad/tests/u1db_tests/test_remote_sync_target.py index 66d404d2..66d404d2 100644 --- a/src/leap/soledad/tests/u1db_tests/test_remote_sync_target.py +++ b/soledad/src/leap/soledad/tests/u1db_tests/test_remote_sync_target.py diff --git a/src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py b/soledad/src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py index 1380e4b1..1380e4b1 100644 --- a/src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py +++ b/soledad/src/leap/soledad/tests/u1db_tests/test_sqlite_backend.py diff --git a/src/leap/soledad/tests/u1db_tests/test_sync.py b/soledad/src/leap/soledad/tests/u1db_tests/test_sync.py index 96aa2736..96aa2736 100644 --- a/src/leap/soledad/tests/u1db_tests/test_sync.py +++ b/soledad/src/leap/soledad/tests/u1db_tests/test_sync.py diff --git a/src/leap/soledad/tests/u1db_tests/testing-certs/Makefile b/soledad/src/leap/soledad/tests/u1db_tests/testing-certs/Makefile index 2385e75b..2385e75b 100644 --- a/src/leap/soledad/tests/u1db_tests/testing-certs/Makefile +++ b/soledad/src/leap/soledad/tests/u1db_tests/testing-certs/Makefile diff --git a/src/leap/soledad/tests/u1db_tests/testing-certs/cacert.pem b/soledad/src/leap/soledad/tests/u1db_tests/testing-certs/cacert.pem index c019a730..c019a730 100644 --- a/src/leap/soledad/tests/u1db_tests/testing-certs/cacert.pem +++ b/soledad/src/leap/soledad/tests/u1db_tests/testing-certs/cacert.pem diff --git a/src/leap/soledad/tests/u1db_tests/testing-certs/testing.cert b/soledad/src/leap/soledad/tests/u1db_tests/testing-certs/testing.cert index 985684fb..985684fb 100644 --- a/src/leap/soledad/tests/u1db_tests/testing-certs/testing.cert +++ b/soledad/src/leap/soledad/tests/u1db_tests/testing-certs/testing.cert diff --git a/src/leap/soledad/tests/u1db_tests/testing-certs/testing.key b/soledad/src/leap/soledad/tests/u1db_tests/testing-certs/testing.key index d83d4920..d83d4920 100644 --- a/src/leap/soledad/tests/u1db_tests/testing-certs/testing.key +++ b/soledad/src/leap/soledad/tests/u1db_tests/testing-certs/testing.key diff --git a/soledad_server/pkg/soledad b/soledad_server/pkg/soledad new file mode 100644 index 00000000..b1d898cc --- /dev/null +++ b/soledad_server/pkg/soledad @@ -0,0 +1,72 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: soledad +# Required-Start: $network $named $remote_fs $syslog $time +# Required-Stop: $network $named $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start soledad daemon at boot time +# Description: Synchronization of locally encrypted data among devices +### END INIT INFO + +PATH=/sbin:/bin:/usr/sbin:/usr/bin +PIDFILE=/var/run/soledad.pid +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.pem +TWISTD_PATH=/usr/bin/twistd +HOME=/var/lib/soledad/ + +[ -r /etc/default/soledad ] && . /etc/default/soledad + +test -r /etc/leap/ || exit 0 + +. /lib/lsb/init-functions + + +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 + echo "." + ;; + + stop) + echo -n "Stopping soledad: twistd" + start-stop-daemon --stop --quiet \ + --pidfile $PIDFILE + echo "." + ;; + + restart) + $0 stop + $0 start + ;; + + force-reload) + $0 restart + ;; + + status) + status_of_proc -p $PIDFILE $TWISTD_PATH soledad && exit 0 || exit $? + ;; + + *) + echo "Usage: /etc/init.d/soledad {start|stop|restart|force-reload|status}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/soledad_server/setup.py b/soledad_server/setup.py new file mode 100644 index 00000000..11aa4648 --- /dev/null +++ b/soledad_server/setup.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# setup.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/>. + + +import os +from setuptools import ( + setup, + find_packages +) + + +install_requirements = [ + 'configparser', + 'couchdb', + 'simplejson', + 'twisted>=12.0.0', # TODO: maybe we just want twisted-web? + 'oauth', # this is not strictly needed by us, but we need it + # until u1db adds it to its release as a dep. + 'u1db', + 'six==1.1.0', + 'routes', + 'PyOpenSSL', + 'leap.soledad>=0.2.1', +] + + +if os.environ.get('VIRTUAL_ENV', None): + data_files = None +else: + # XXX this should go only for linux/mac + # data_files = [("/etc/init.d/", ["pkg/soledad"])] + +trove_classifiers = ( + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: " + "GNU General Public License v3 or later (GPLv3+)", + "Environment :: Console", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Topic :: Database :: Front-Ends", + "Topic :: Software Development :: Libraries :: Python Modules" +) + +setup( + name='leap.soledad_server', + version='0.2.2', + url='https://leap.se/', + license='GPLv3+', + description='Synchronization of locally encrypted data among devices.', + author='The LEAP Encryption Access Project', + author_email='info@leap.se', + long_description=( + "Soledad is the part of LEAP that allows application data to be " + "securely shared among devices. It provides, to other parts of the " + "LEAP client, an API for data storage and sync." + ), + namespace_packages=["leap"], + 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/__init__.py b/soledad_server/src/leap/__init__.py new file mode 100644 index 00000000..f48ad105 --- /dev/null +++ b/soledad_server/src/leap/__init__.py @@ -0,0 +1,6 @@ +# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages +try: + __import__('pkg_resources').declare_namespace(__name__) +except ImportError: + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/src/leap/soledad/server.py b/soledad_server/src/leap/soledad_server/__init__.py index 9c9e0ad7..bea5d5fd 100644 --- a/src/leap/soledad/server.py +++ b/soledad_server/src/leap/soledad_server/__init__.py @@ -20,7 +20,7 @@ A U1DB server that stores data using CouchDB as its persistence layer. This should be run with: - twistd -n web --wsgi=leap.soledad.server.application --port=2424 + twistd -n web --wsgi=leap.soledad_server.application --port=2424 """ import configparser @@ -53,7 +53,7 @@ if version.base() == "12.0.0": from couchdb.client import Server from leap.soledad import SECRETS_DOC_ID_HASH_PREFIX -from leap.soledad.backends.couch import CouchServerState +from leap.soledad_server.couch import CouchServerState #----------------------------------------------------------------------------- @@ -126,7 +126,7 @@ class URLToAuth(object): @return: The database name corresponding to C{uuid}. @rtype: str """ - return sha256('%s%s' % (SECRETS_DOC_ID_HASH_PREFIX, uuid)).hexdigest() + return '%s%s' % (SoledadApp.USER_DB_PREFIX, uuid) def _register_auth_info(self, dbname): """ @@ -329,6 +329,11 @@ class SoledadApp(http_app.HTTPApp): The name of the shared database that holds user's encrypted secrets. """ + USER_DB_PREFIX = 'uuid-' + """ + The string prefix of users' databases. + """ + def __call__(self, environ, start_response): """ Handle a WSGI call to the Soledad application. diff --git a/src/leap/soledad/backends/couch.py b/soledad_server/src/leap/soledad_server/couch.py index 57885012..ed5ad6b3 100644 --- a/src/leap/soledad/backends/couch.py +++ b/soledad_server/src/leap/soledad_server/couch.py @@ -34,11 +34,10 @@ from couchdb.client import Server, Document as CouchDocument from couchdb.http import ResourceNotFound -from leap.soledad.backends.objectstore import ( +from leap.soledad_server.objectstore import ( ObjectStoreDatabase, ObjectStoreSyncTarget, ) -from leap.soledad.backends.leap_backend import LeapDocument class InvalidURLError(Exception): @@ -120,10 +119,7 @@ class CouchDatabase(ObjectStoreDatabase): except ResourceNotFound: self._server.create(self._dbname) self._database = self._server[self._dbname] - ObjectStoreDatabase.__init__(self, replica_uid=replica_uid, - # TODO: move the factory choice - # away - document_factory=LeapDocument) + ObjectStoreDatabase.__init__(self, replica_uid=replica_uid) #------------------------------------------------------------------------- # methods from Database diff --git a/src/leap/soledad/backends/objectstore.py b/soledad_server/src/leap/soledad_server/objectstore.py index 8afac3ec..8afac3ec 100644 --- a/src/leap/soledad/backends/objectstore.py +++ b/soledad_server/src/leap/soledad_server/objectstore.py diff --git a/src/leap/soledad/backends/__init__.py b/src/leap/soledad/backends/__init__.py deleted file mode 100644 index 720a8118..00000000 --- a/src/leap/soledad/backends/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# __init__.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/>. - - -""" -Backends that extend U1DB functionality. -""" - -from leap.soledad.backends import ( - objectstore, - couch, - sqlcipher, - leap_backend, -) - - -__all__ = [ - 'objectstore', - 'couch', - 'sqlcipher', - 'leap_backend', -] |