]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- added new backend for pysqlcipher, as we will probably get
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 29 Oct 2014 18:55:42 +0000 (14:55 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 29 Oct 2014 18:55:42 +0000 (14:55 -0400)
requests for it soon.

doc/build/changelog/changelog_09.rst
doc/build/dialects/sqlite.rst
lib/sqlalchemy/dialects/sqlite/__init__.py
lib/sqlalchemy/dialects/sqlite/pysqlcipher.py [new file with mode: 0644]
test/requirements.py

index fe8dc015057e6f84b2d35b2e28aed63f0b4b9a21..6909da357035b358c84a0c0756216d183c0c8f62 100644 (file)
 .. changelog::
     :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
index 21fd4e3aa5234c44149b31527db29dcceb930214..a18b0ba7b8aea921baa2a38448524c1f62fc43c1 100644 (file)
@@ -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
index 0eceaa53704fefce3c6938a23b5abf5f76e9ab0d..a53d53e9d17e8479582cdda2018baaa6c4c09d62 100644 (file)
@@ -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 (file)
index 0000000..3c55a1d
--- /dev/null
@@ -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
index 0a695b641f2bbb3ccd3d8f4573924495969d88be..05ca8d717acdbae0ee800cde89b3781860a1a209 100644 (file)
@@ -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):