diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5a6f88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*pyc +*egg-info +*egg diff --git a/README.md b/README.md index 7a649c6..39c7373 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**django-sqlcipher** +# django-sqlcipher SQLCipher is an SQLite extension that provides transparent 256-bit AES encryption of database files. @@ -8,7 +8,7 @@ This app does it for you. You only need to specify the database key in your proj For more about SQLCipher take a look at [http://sqlcipher.net/](http://sqlcipher.net/). -**Requirements** +## Requirements * python-sqlcipher (Python compiled with SQLCipher support) @@ -16,13 +16,13 @@ For more about python-sqlcipher take a look at: [https://code.launchpad.net/~jplacerda/+junk/python-sqlcipher](https://code.launchpad.net/~jplacerda/+junk/python-sqlcipher) -**Installation** +## Installation `pip install git+http://github.com/codasus/django-sqlcipher#egg=sqlcipher` Or manually place it on your `PYTHON_PATH`. -**Configuration** +## Configuration Open your project's `settings.py` file and: @@ -30,11 +30,18 @@ Open your project's `settings.py` file and: 2. Set your database engine to `sqlcipher.backend`. -3. Put the following line where you want: +3. Optionally, type your key into your settings file (**unsafe**): `PRAGMA_KEY = "YOUR DATABASE KEY"` -**MIT License** +You may not wish to expose your encryption key in a file. +django-sqlcipher ships with custom management commands that will prompt +for the key when invoking `runserver` and `migrate`, however you +should consider how you want to set `django.conf.settings.PRAGMA_KEY` +at runtime in your production environment. + + +## MIT License
Copyright (c) 2011 Caio Ariede and Codasus Technologies.
diff --git a/setup.py b/setup.py
index bc28ef4..96ba0ee 100644
--- a/setup.py
+++ b/setup.py
@@ -1,11 +1,12 @@
-from distutils.core import setup
-from distutils.command.install import INSTALL_SCHEMES
import os
+from distutils.command.install import INSTALL_SCHEMES
+from setuptools import setup, find_packages
+
root = os.path.dirname(os.path.abspath(__file__))
os.chdir(root)
-VERSION = '0.1.1'
+VERSION = '0.1.2'
# Make data go to the right place.
# http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb
@@ -23,7 +24,7 @@
url="http://github.com/codasus/django-sqlcipher",
license="Creative Commons Attribution-ShareAlike 3.0 Unported License",
platforms=["any"],
- packages=['sqlcipher'],
+ packages=find_packages(),
classifiers=[
"Development Status :: 3 - Alpha",
"Framework :: Django",
diff --git a/sqlcipher/backend/base.py b/sqlcipher/backend/base.py
index c0b2b38..99ba50e 100644
--- a/sqlcipher/backend/base.py
+++ b/sqlcipher/backend/base.py
@@ -1,10 +1,121 @@
-from django.db.backends.sqlite3.base import DatabaseWrapper as BaseDatabaseWrapper
+from __future__ import unicode_literals
+
+from django.db.backends.sqlite3.base import DatabaseWrapper as BaseDatabaseWrapper, \
+ _sqlite_date_extract, _sqlite_date_trunc, _sqlite_datetime_cast_date, \
+ _sqlite_datetime_extract, _sqlite_datetime_trunc, _sqlite_time_extract, \
+ _sqlite_regexp, _sqlite_format_dtdelta, _sqlite_power, FORMAT_QMARK_REGEX
from ..signals import setup
+from pysqlcipher import dbapi2 as Database
+
+
+import datetime
+import decimal
+import warnings
+
+from django.conf import settings
+from django.db.backends import utils as backend_utils
+from django.utils import six, timezone
+from django.utils.dateparse import (
+ parse_date, parse_datetime, parse_time,
+)
+from django.utils.deprecation import RemovedInDjango20Warning
+from django.utils.safestring import SafeBytes
+
+try:
+ import pytz
+except ImportError:
+ pytz = None
+
+DatabaseError = Database.DatabaseError
+IntegrityError = Database.IntegrityError
+
+
+def adapt_datetime_warn_on_aware_datetime(value):
+ # Remove this function and rely on the default adapter in Django 2.0.
+ if settings.USE_TZ and timezone.is_aware(value):
+ warnings.warn(
+ "The SQLite database adapter received an aware datetime (%s), "
+ "probably from cursor.execute(). Update your code to pass a "
+ "naive datetime in the database connection's time zone (UTC by "
+ "default).", RemovedInDjango20Warning)
+ # This doesn't account for the database connection's timezone,
+ # which isn't known. (That's why this adapter is deprecated.)
+ value = value.astimezone(timezone.utc).replace(tzinfo=None)
+ return value.isoformat(str(" "))
+
+
+def decoder(conv_func):
+ """ The Python sqlite3 interface returns always byte strings.
+ This function converts the received value to a regular string before
+ passing it to the receiver function.
+ """
+ return lambda s: conv_func(s.decode('utf-8'))
+
+
+Database.register_converter(str("bool"), decoder(lambda s: s == '1'))
+Database.register_converter(str("time"), decoder(parse_time))
+Database.register_converter(str("date"), decoder(parse_date))
+Database.register_converter(str("datetime"), decoder(parse_datetime))
+Database.register_converter(str("timestamp"), decoder(parse_datetime))
+Database.register_converter(str("TIMESTAMP"), decoder(parse_datetime))
+Database.register_converter(str("decimal"), decoder(backend_utils.typecast_decimal))
+
+Database.register_adapter(datetime.datetime, adapt_datetime_warn_on_aware_datetime)
+Database.register_adapter(decimal.Decimal, backend_utils.rev_typecast_decimal)
+if six.PY2:
+ Database.register_adapter(str, lambda s: s.decode('utf-8'))
+ Database.register_adapter(SafeBytes, lambda s: s.decode('utf-8'))
+
class DatabaseWrapper(BaseDatabaseWrapper):
- def _cursor(self):
- if self.connection is None:
- setup()
- return super(DatabaseWrapper, self)._cursor()
+ Database = Database
+
+ def create_cursor(self, name=None):
+ if name:
+ base_cursor = super(DatabaseWrapper, self).create_cursor(name)
+ else:
+ base_cursor = super(DatabaseWrapper, self).create_cursor()
+ return SQLiteCursorWrapper(base_cursor)
+
+ # def _cursor(self, *args, **kwargs):
+ # if self.connection is None:
+ # setup()
+ # return super(DatabaseWrapper, self)._cursor(*args, **kwargs)
+
+ def get_new_connection(self, conn_params):
+ conn = Database.connect(**conn_params)
+ conn.create_function("django_date_extract", 2, _sqlite_date_extract)
+ conn.create_function("django_date_trunc", 2, _sqlite_date_trunc)
+ conn.create_function("django_datetime_cast_date", 2, _sqlite_datetime_cast_date)
+ conn.create_function("django_datetime_extract", 3, _sqlite_datetime_extract)
+ conn.create_function("django_datetime_trunc", 3, _sqlite_datetime_trunc)
+ conn.create_function("django_time_extract", 2, _sqlite_time_extract)
+ conn.create_function("regexp", 2, _sqlite_regexp)
+ conn.create_function("django_format_dtdelta", 3, _sqlite_format_dtdelta)
+ conn.create_function("django_power", 2, _sqlite_power)
+ return conn
+
+ def create_cursor(self, *args, **kwargs):
+ return self.connection.cursor(factory=SQLiteCursorWrapper)
+
+
+class SQLiteCursorWrapper(Database.Cursor):
+ """
+ Django uses "format" style placeholders, but pysqlite2 uses "qmark" style.
+ This fixes it -- but note that if you want to use a literal "%s" in a query,
+ you'll need to use "%%s".
+ """
+ def execute(self, query, params=None):
+ if params is None:
+ return Database.Cursor.execute(self, query)
+ query = self.convert_query(query)
+ return Database.Cursor.execute(self, query, params)
+
+ def executemany(self, query, param_list):
+ query = self.convert_query(query)
+ return Database.Cursor.executemany(self, query, param_list)
+
+ def convert_query(self, query):
+ return FORMAT_QMARK_REGEX.sub('?', query).replace('%%', '%')
diff --git a/sqlcipher/management/__init__.py b/sqlcipher/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sqlcipher/management/commands/__init__.py b/sqlcipher/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sqlcipher/management/commands/_mixins.py b/sqlcipher/management/commands/_mixins.py
new file mode 100644
index 0000000..f0a60ab
--- /dev/null
+++ b/sqlcipher/management/commands/_mixins.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from __future__ import print_function
+
+
+from sqlcipher.utils import ensure_pragma_key
+
+
+class PromptForPragmaKeyMixin(object):
+ """""
+ This is a universal command that you can have other management commands
+ inherit from in case they need database access.
+ """
+
+ def handle(self, *args, **options):
+ ensure_pragma_key()
+ super(PromptForPragmaKeyMixin, self).handle(*args, **options)
diff --git a/sqlcipher/management/commands/dumpdata.py b/sqlcipher/management/commands/dumpdata.py
new file mode 100644
index 0000000..fe675b0
--- /dev/null
+++ b/sqlcipher/management/commands/dumpdata.py
@@ -0,0 +1,6 @@
+from django.core.management.commands.dumpdata import Command as DumpdataCommand
+
+from ._mixins import PromptForPragmaKeyMixin
+
+class Command(PromptForPragmaKeyMixin, DumpdataCommand):
+ pass
diff --git a/sqlcipher/management/commands/loaddata.py b/sqlcipher/management/commands/loaddata.py
new file mode 100644
index 0000000..31ad8b8
--- /dev/null
+++ b/sqlcipher/management/commands/loaddata.py
@@ -0,0 +1,6 @@
+from django.core.management.commands.loaddata import Command as LoaddataCommand
+
+from ._mixins import PromptForPragmaKeyMixin
+
+class Command(PromptForPragmaKeyMixin, LoaddataCommand):
+ pass
diff --git a/sqlcipher/management/commands/migrate.py b/sqlcipher/management/commands/migrate.py
new file mode 100644
index 0000000..51d1a5b
--- /dev/null
+++ b/sqlcipher/management/commands/migrate.py
@@ -0,0 +1,12 @@
+
+from django.core.management.commands.migrate import Command as BaseCommand
+
+from ._mixins import PromptForPragmaKeyMixin
+
+
+class Command(PromptForPragmaKeyMixin, BaseCommand):
+ """
+ Before migrating, we need to know the pragma key to access the database. If
+ it does not exist, retrieve it from command line input.
+ """
+ pass
diff --git a/sqlcipher/management/commands/runserver.py b/sqlcipher/management/commands/runserver.py
new file mode 100644
index 0000000..7f20613
--- /dev/null
+++ b/sqlcipher/management/commands/runserver.py
@@ -0,0 +1,10 @@
+from django.core.management.commands.runserver import Command as RunserverCommand
+
+from ._mixins import ensure_pragma_key
+
+
+class Command(RunserverCommand):
+
+ def inner_run(self, *args, **options):
+ ensure_pragma_key()
+ RunserverCommand.inner_run(self, *args, **options)
diff --git a/sqlcipher/signals.py b/sqlcipher/signals.py
index b5087a3..4d9887e 100644
--- a/sqlcipher/signals.py
+++ b/sqlcipher/signals.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
from django.conf import settings
from django.db.backends.signals import connection_created
diff --git a/sqlcipher/utils.py b/sqlcipher/utils.py
new file mode 100644
index 0000000..5883b33
--- /dev/null
+++ b/sqlcipher/utils.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from __future__ import print_function
+
+import sys
+
+from django.conf import settings
+from getpass import getpass
+
+
+def ensure_pragma_key():
+ if not hasattr(settings, 'PRAGMA_KEY') or not settings.PRAGMA_KEY:
+ sys.stderr.write("There is no SQL Cipher key defined, it's unsafe to store in your settings. Please input your key.\n\n")
+ key = getpass("Key: ")
+ settings.PRAGMA_KEY = key.decode("utf-8")