summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2014-10-29 14:55:42 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2014-10-29 14:55:42 -0400
commitd2c1edfb15334a2fb6ada5b064563c144ac22ad7 (patch)
tree96f8e7b51dd57d30740cbc017fa66e71ee229f5f
parente1d1d999c9a688f4c8dbbe885438c63d6ef494c6 (diff)
downloadsqlalchemy-d2c1edfb15334a2fb6ada5b064563c144ac22ad7.tar.gz
- added new backend for pysqlcipher, as we will probably get
requests for it soon.
-rw-r--r--doc/build/changelog/changelog_09.rst12
-rw-r--r--doc/build/dialects/sqlite.rst7
-rw-r--r--lib/sqlalchemy/dialects/sqlite/__init__.py2
-rw-r--r--lib/sqlalchemy/dialects/sqlite/pysqlcipher.py116
-rw-r--r--test/requirements.py4
5 files changed, 137 insertions, 4 deletions
diff --git a/doc/build/changelog/changelog_09.rst b/doc/build/changelog/changelog_09.rst
index fe8dc0150..6909da357 100644
--- a/doc/build/changelog/changelog_09.rst
+++ b/doc/build/changelog/changelog_09.rst
@@ -14,6 +14,18 @@
:version: 0.9.9
.. change::
+ :tags: feature, sqlite
+ :versions: 1.0.0
+
+ Added a new SQLite backend for the SQLCipher backend. This backend
+ provides for encrypted SQLite databases using the pysqlcipher Python
+ driver, which is very similar to the pysqlite driver.
+
+ .. seealso::
+
+ :mod:`~sqlalchemy.dialects.sqlite.pysqlcipher`
+
+ .. change::
:tags: bug, orm
:tickets: 3232
:versions: 1.0.0
diff --git a/doc/build/dialects/sqlite.rst b/doc/build/dialects/sqlite.rst
index 21fd4e3aa..a18b0ba7b 100644
--- a/doc/build/dialects/sqlite.rst
+++ b/doc/build/dialects/sqlite.rst
@@ -28,4 +28,9 @@ they originate from :mod:`sqlalchemy.types` or from the local dialect::
Pysqlite
--------
-.. automodule:: sqlalchemy.dialects.sqlite.pysqlite \ No newline at end of file
+.. automodule:: sqlalchemy.dialects.sqlite.pysqlite
+
+Pysqlcipher
+-----------
+
+.. automodule:: sqlalchemy.dialects.sqlite.pysqlcipher \ No newline at end of file
diff --git a/lib/sqlalchemy/dialects/sqlite/__init__.py b/lib/sqlalchemy/dialects/sqlite/__init__.py
index 0eceaa537..a53d53e9d 100644
--- a/lib/sqlalchemy/dialects/sqlite/__init__.py
+++ b/lib/sqlalchemy/dialects/sqlite/__init__.py
@@ -5,7 +5,7 @@
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-from sqlalchemy.dialects.sqlite import base, pysqlite
+from sqlalchemy.dialects.sqlite import base, pysqlite, pysqlcipher
# default dialect
base.dialect = pysqlite.dialect
diff --git a/lib/sqlalchemy/dialects/sqlite/pysqlcipher.py b/lib/sqlalchemy/dialects/sqlite/pysqlcipher.py
new file mode 100644
index 000000000..3c55a1de7
--- /dev/null
+++ b/lib/sqlalchemy/dialects/sqlite/pysqlcipher.py
@@ -0,0 +1,116 @@
+# sqlite/pysqlcipher.py
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+"""
+.. dialect:: sqlite+pysqlcipher
+ :name: pysqlcipher
+ :dbapi: pysqlcipher
+ :connectstring: sqlite+pysqlcipher://:passphrase/file_path[?kdf_iter=<iter>]
+ :url: https://pypi.python.org/pypi/pysqlcipher
+
+ ``pysqlcipher`` is a fork of the standard ``pysqlite`` driver to make
+ use of the `SQLCipher <https://www.zetetic.net/sqlcipher>`_ backend.
+
+ .. versionadded:: 0.9.9
+
+Driver
+------
+
+The driver here is the `pysqlcipher <https://pypi.python.org/pypi/pysqlcipher>`_
+driver, which makes use of the SQLCipher engine. This system essentially
+introduces new PRAGMA commands to SQLite which allows the setting of a
+passphrase and other encryption parameters, allowing the database
+file to be encrypted.
+
+Connect Strings
+---------------
+
+The format of the connect string is in every way the same as that
+of the :mod:`~sqlalchemy.dialects.sqlite.pysqlite` driver, except that the
+"password" field is now accepted, which should contain a passphrase::
+
+ e = create_engine('sqlite+pysqlcipher://:testing@/foo.db')
+
+For an absolute file path, two leading slashes should be used for the
+database name::
+
+ e = create_engine('sqlite+pysqlcipher://:testing@//path/to/foo.db')
+
+A selection of additional encryption-related pragmas supported by SQLCipher
+as documented at https://www.zetetic.net/sqlcipher/sqlcipher-api/ can be passed
+in the query string, and will result in that PRAGMA being called for each
+new connection. Currently, ``cipher``, ``kdf_iter``
+``cipher_page_size`` and ``cipher_use_hmac`` are supported::
+
+ e = create_engine('sqlite+pysqlcipher://:testing@/foo.db?cipher=aes-256-cfb&kdf_iter=64000')
+
+
+Pooling Behavior
+----------------
+
+The driver makes a change to the default pool behavior of pysqlite
+as described in :ref:`pysqlite_threading_pooling`. The pysqlcipher driver
+has been observed to be significantly slower on connection than the
+pysqlite driver, most likely due to the encryption overhead, so the
+dialect here defaults to using the :class:`.SingletonThreadPool`
+implementation,
+instead of the :class:`.NullPool` pool used by pysqlite. As always, the pool
+implementation is entirely configurable using the
+:paramref:`.create_engine.poolclass` parameter; the :class:`.StaticPool` may
+be more feasible for single-threaded use, or :class:`.NullPool` may be used
+to prevent unencrypted connections from being held open for long periods of
+time, at the expense of slower startup time for new connections.
+
+
+"""
+from __future__ import absolute_import
+from .pysqlite import SQLiteDialect_pysqlite
+from ...engine import url as _url
+from ... import pool
+
+
+class SQLiteDialect_pysqlcipher(SQLiteDialect_pysqlite):
+ driver = 'pysqlcipher'
+
+ pragmas = ('kdf_iter', 'cipher', 'cipher_page_size', 'cipher_use_hmac')
+
+ @classmethod
+ def dbapi(cls):
+ from pysqlcipher import dbapi2 as sqlcipher
+ return sqlcipher
+
+ @classmethod
+ def get_pool_class(cls, url):
+ return pool.SingletonThreadPool
+
+ def connect(self, *cargs, **cparams):
+ passphrase = cparams.pop('passphrase', '')
+
+ pragmas = dict(
+ (key, cparams.pop(key, None)) for key in
+ self.pragmas
+ )
+
+ conn = super(SQLiteDialect_pysqlcipher, self).\
+ connect(*cargs, **cparams)
+ conn.execute('pragma key="%s"' % passphrase)
+ for prag, value in pragmas.items():
+ if value is not None:
+ conn.execute('pragma %s=%s' % (prag, value))
+
+ return conn
+
+ def create_connect_args(self, url):
+ super_url = _url.URL(
+ url.drivername, username=url.username,
+ host=url.host, database=url.database, query=url.query)
+ c_args, opts = super(SQLiteDialect_pysqlcipher, self).\
+ create_connect_args(super_url)
+ opts['passphrase'] = url.password
+ return c_args, opts
+
+dialect = SQLiteDialect_pysqlcipher
diff --git a/test/requirements.py b/test/requirements.py
index 0a695b641..05ca8d717 100644
--- a/test/requirements.py
+++ b/test/requirements.py
@@ -449,7 +449,7 @@ class DefaultRequirements(SuiteRequirements):
after an insert() construct executes.
"""
return fails_on_everything_except('mysql',
- 'sqlite+pysqlite',
+ 'sqlite+pysqlite', 'sqlite+pysqlcipher',
'sybase', 'mssql')
@property
@@ -466,7 +466,7 @@ class DefaultRequirements(SuiteRequirements):
"""
return skip_if('mssql+pymssql', 'crashes on pymssql') + \
fails_on_everything_except('mysql',
- 'sqlite+pysqlite')
+ 'sqlite+pysqlite', 'sqlite+pysqlcipher')
@property
def sane_multi_rowcount(self):