123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- # sqlite/pysqlcipher.py
- # Copyright (C) 2005-2022 the SQLAlchemy authors and contributors
- # <see AUTHORS file>
- #
- # This module is part of SQLAlchemy and is released under
- # the MIT License: https://www.opensource.org/licenses/mit-license.php
- """
- .. dialect:: sqlite+pysqlcipher
- :name: pysqlcipher
- :dbapi: sqlcipher 3 or pysqlcipher
- :connectstring: sqlite+pysqlcipher://:passphrase@/file_path[?kdf_iter=<iter>]
- Dialect for support of DBAPIs that make use of the
- `SQLCipher <https://www.zetetic.net/sqlcipher>`_ backend.
- Driver
- ------
- Current dialect selection logic is:
- * If the :paramref:`_sa.create_engine.module` parameter supplies a DBAPI module,
- that module is used.
- * Otherwise for Python 3, choose https://pypi.org/project/sqlcipher3/
- * If not available, fall back to https://pypi.org/project/pysqlcipher3/
- * For Python 2, https://pypi.org/project/pysqlcipher/ is used.
- .. warning:: The ``pysqlcipher3`` and ``pysqlcipher`` DBAPI drivers are no
- longer maintained; the ``sqlcipher3`` driver as of this writing appears
- to be current. For future compatibility, any pysqlcipher-compatible DBAPI
- may be used as follows::
- import sqlcipher_compatible_driver
- from sqlalchemy import create_engine
- e = create_engine(
- "sqlite+pysqlcipher://:password@/dbname.db",
- module=sqlcipher_compatible_driver
- )
- These drivers make 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')
- .. warning:: Previous versions of sqlalchemy did not take into consideration
- the encryption-related pragmas passed in the url string, that were silently
- ignored. This may cause errors when opening files saved by a
- previous sqlalchemy version if the encryption options do not match.
- 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:`_sa.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.
- """ # noqa
- from __future__ import absolute_import
- from .pysqlite import SQLiteDialect_pysqlite
- from ... import pool
- from ... import util
- class SQLiteDialect_pysqlcipher(SQLiteDialect_pysqlite):
- driver = "pysqlcipher"
- supports_statement_cache = True
- pragmas = ("kdf_iter", "cipher", "cipher_page_size", "cipher_use_hmac")
- @classmethod
- def dbapi(cls):
- if util.py3k:
- try:
- import sqlcipher3 as sqlcipher
- except ImportError:
- pass
- else:
- return sqlcipher
- from pysqlcipher3 import dbapi2 as sqlcipher
- else:
- from pysqlcipher import dbapi2 as sqlcipher
- return sqlcipher
- @classmethod
- def get_pool_class(cls, url):
- return pool.SingletonThreadPool
- def on_connect_url(self, url):
- super_on_connect = super(
- SQLiteDialect_pysqlcipher, self
- ).on_connect_url(url)
- # pull the info we need from the URL early. Even though URL
- # is immutable, we don't want any in-place changes to the URL
- # to affect things
- passphrase = url.password or ""
- url_query = dict(url.query)
- def on_connect(conn):
- cursor = conn.cursor()
- cursor.execute('pragma key="%s"' % passphrase)
- for prag in self.pragmas:
- value = url_query.get(prag, None)
- if value is not None:
- cursor.execute('pragma %s="%s"' % (prag, value))
- cursor.close()
- if super_on_connect:
- super_on_connect(conn)
- return on_connect
- def create_connect_args(self, url):
- plain_url = url._replace(password=None)
- plain_url = plain_url.difference_update_query(self.pragmas)
- return super(SQLiteDialect_pysqlcipher, self).create_connect_args(
- plain_url
- )
- dialect = SQLiteDialect_pysqlcipher
|