├── .coveragerc ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CHANGES.rst ├── CONTRIBUTING.rst ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── doc ├── Makefile ├── conf.py └── index.rst ├── pgcrypto_expressions ├── __init__.py ├── fields.py ├── funcs.py └── test │ ├── __init__.py │ ├── models.py │ ├── settings.py │ ├── test_fields.py │ ├── test_funcs.py │ └── utils.py ├── requirements.txt ├── runtests.py ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = pgcrypto_expressions 3 | omit = pgcrypto_expressions/test/* 4 | branch = 1 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | django_pgcrypto_expressions.egg-info/ 2 | htmlcov/ 3 | .tox/ 4 | doc/_build/ 5 | dist/ 6 | .coverage 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | env: 4 | global: 5 | - DJPGC_USERNAME=postgres 6 | # Keep this list up to date using `tox -l` 7 | matrix: 8 | - TOXENV=py27-django18 9 | - TOXENV=py34-django18 10 | - TOXENV=pypy-django18 11 | - TOXENV=pypy3-django18 12 | - TOXENV=py27-django19 13 | - TOXENV=py34-django19 14 | - TOXENV=pypy-django19 15 | - TOXENV=flake8 16 | - TOXENV=docs 17 | 18 | install: travis_retry pip install tox coveralls 19 | 20 | before_script: 21 | - psql -c 'create extension pgcrypto;' -U postgres template1 22 | 23 | script: tox -e $TOXENV 24 | 25 | after_success: coveralls 26 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Carl Meyer 2 | Joey Wilhelm 3 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ======= 3 | 4 | master (unreleased) 5 | ------------------- 6 | 7 | * Drop support for Python 3.2 and 3.3. 8 | 9 | * Allow setting PGCRYPTO_KEY per-database in a multi-db setup. Thanks Joey 10 | Wilhelm. Merge of GH-4, fixes GH-2. 11 | 12 | 13 | 0.2 (2016.01.04) 14 | ---------------- 15 | 16 | * Fix use of a secret with a percent symbol in it. Thanks Joey Wilhelm for the 17 | report. Merge of GH-3, fixes GH-1. 18 | 19 | 20 | 0.1 (2015.05.27) 21 | ---------------- 22 | 23 | * Initial working version. 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Thanks for your interest in contributing! The advice below will help you get 5 | your issue fixed / pull request merged. 6 | 7 | Please file bugs and send pull requests to the `GitHub repository`_ and `issue 8 | tracker`_. 9 | 10 | .. _GitHub repository: https://github.com/orcasgit/django-pgcrypto-expressions/ 11 | .. _issue tracker: https://github.com/orcasgit/django-pgcrypto-expressions/issues 12 | 13 | 14 | 15 | Submitting Issues 16 | ----------------- 17 | 18 | Issues are easier to reproduce/resolve when they have: 19 | 20 | - A pull request with a failing test demonstrating the issue 21 | - A code example that produces the issue consistently 22 | - A traceback (when applicable) 23 | 24 | 25 | Pull Requests 26 | ------------- 27 | 28 | When creating a pull request: 29 | 30 | - Write tests (see below) 31 | - Note user-facing changes in the `CHANGES`_ file 32 | - Update the documentation as needed 33 | - Add yourself to the `AUTHORS`_ file 34 | 35 | .. _AUTHORS: AUTHORS.rst 36 | .. _CHANGES: CHANGES.rst 37 | 38 | 39 | Testing 40 | ------- 41 | 42 | Please add tests for any changes you submit. The tests should fail before your 43 | code changes, and pass with your changes. Existing tests should not 44 | break. Coverage (see below) should remain at 100% following a full tox run. 45 | 46 | To install all the requirements for running the tests:: 47 | 48 | pip install -r requirements.txt 49 | 50 | The tests also require that you have a local PostgreSQL server running, whose 51 | ``template`` database contains the ``pgcrypto`` extension. (Run ``sudo -u 52 | postgres psql template1 -c 'CREATE EXTENSION pgcrypto;'`` to add it.) The tests 53 | will use a database named ``djpgcetest``; if it already exists it will be wiped 54 | and re-created. 55 | 56 | To run the tests once:: 57 | 58 | ./runtests.py 59 | 60 | To run tox (which runs the tests across all supported Python and Django 61 | versions) and generate a coverage report in the ``htmlcov/`` directory:: 62 | 63 | make test 64 | 65 | This requires that you have ``python2.7``, ``python3.2``, ``python3.3``, 66 | ``python3.4``, ``pypy``, and ``pypy3`` binaries on your system's shell path. 67 | 68 | To install PostgreSQL on Debian-based systems:: 69 | 70 | $ sudo apt-get install postgresql 71 | 72 | You'll need to run the tests as a user with permission to create databases. By 73 | default, the tests attempt to connect as a user with your shell username. You 74 | can override this by setting the environment variable ``DJPGC_USERNAME``. 75 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 ORCAS, Inc 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CHANGES.rst 3 | include LICENSE.txt 4 | include MANIFEST.in 5 | include README.rst 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | coverage erase 3 | tox 4 | coverage html 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | django-pgcrypto-expressions 3 | =========================== 4 | 5 | .. image:: https://secure.travis-ci.org/orcasgit/django-pgcrypto-expressions.png?branch=master 6 | :target: http://travis-ci.org/orcasgit/django-pgcrypto-expressions 7 | :alt: Test status 8 | .. image:: https://coveralls.io/repos/orcasgit/django-pgcrypto-expressions/badge.png?branch=master 9 | :target: https://coveralls.io/r/orcasgit/django-pgcrypto-expressions 10 | :alt: Test coverage 11 | .. image:: https://readthedocs.org/projects/django-pgcrypto-expressions/badge/?version=latest 12 | :target: https://readthedocs.org/projects/django-pgcrypto-expressions/?badge=latest 13 | :alt: Documentation Status 14 | .. image:: https://badge.fury.io/py/django-pgcrypto-expressions.svg 15 | :target: https://pypi.python.org/pypi/django-pgcrypto-expressions 16 | :alt: Latest version 17 | 18 | Django model fields whose values are encrypted using the ``pgp_sym_encrypt`` 19 | function from `pgcrypto`_. 20 | 21 | ``django-pgcrypto-expressions`` supports `Django`_ 1.8.2 and later on Python 22 | 2.7, 3.2, 3.3, 3.4, pypy, and pypy3. 23 | 24 | .. _Django: http://www.djangoproject.com/ 25 | .. _pgcrypto: http://www.postgresql.org/docs/9.4/static/pgcrypto.html 26 | 27 | 28 | Getting Help 29 | ============ 30 | 31 | Documentation for django-pgcrypto-expressions is available at 32 | https://django-pgcrypto-expressions.readthedocs.org/ 33 | 34 | This app is available on `PyPI`_ and can be installed with ``pip install 35 | django-pgcrypto-expressions``. 36 | 37 | .. _PyPI: https://pypi.python.org/pypi/django-pgcrypto-expressions/ 38 | 39 | 40 | Contributing 41 | ============ 42 | 43 | See the `contributing docs`_. 44 | 45 | .. _contributing docs: https://github.com/orcasgit/django-pgcrypto-expressions/blob/master/CONTRIBUTING.rst 46 | 47 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-pgcrypto-expressions.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-pgcrypto-expressions.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-pgcrypto-expressions" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-pgcrypto-expressions" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # django-pgcrypto-expressions documentation build configuration file, created by 5 | # sphinx-quickstart on Wed May 27 13:54:15 2015. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import shlex 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix(es) of source filenames. 39 | # You can specify multiple suffix as a list of string: 40 | # source_suffix = ['.rst', '.md'] 41 | source_suffix = '.rst' 42 | 43 | # The encoding of source files. 44 | #source_encoding = 'utf-8-sig' 45 | 46 | # The master toctree document. 47 | master_doc = 'index' 48 | 49 | # General information about the project. 50 | project = 'django-pgcrypto-expressions' 51 | copyright = '2015, Carl Meyer' 52 | author = 'Carl Meyer' 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The short X.Y version. 59 | version = '0.1' 60 | # The full version, including alpha/beta/rc tags. 61 | release = '0.1' 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # 66 | # This is also used if you do content translation via gettext catalogs. 67 | # Usually you set "language" from the command line for these cases. 68 | language = None 69 | 70 | # There are two options for replacing |today|: either, you set today to some 71 | # non-false value, then it is used: 72 | #today = '' 73 | # Else, today_fmt is used as the format for a strftime call. 74 | #today_fmt = '%B %d, %Y' 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | exclude_patterns = ['_build'] 79 | 80 | # The reST default role (used for this markup: `text`) to use for all 81 | # documents. 82 | #default_role = None 83 | 84 | # If true, '()' will be appended to :func: etc. cross-reference text. 85 | #add_function_parentheses = True 86 | 87 | # If true, the current module name will be prepended to all description 88 | # unit titles (such as .. function::). 89 | #add_module_names = True 90 | 91 | # If true, sectionauthor and moduleauthor directives will be shown in the 92 | # output. They are ignored by default. 93 | #show_authors = False 94 | 95 | # The name of the Pygments (syntax highlighting) style to use. 96 | pygments_style = 'sphinx' 97 | 98 | # A list of ignored prefixes for module index sorting. 99 | #modindex_common_prefix = [] 100 | 101 | # If true, keep warnings as "system message" paragraphs in the built documents. 102 | #keep_warnings = False 103 | 104 | # If true, `todo` and `todoList` produce output, else they produce nothing. 105 | todo_include_todos = False 106 | 107 | 108 | # -- Options for HTML output ---------------------------------------------- 109 | 110 | # The theme to use for HTML and HTML Help pages. See the documentation for 111 | # a list of builtin themes. 112 | html_theme = 'alabaster' 113 | 114 | # Theme options are theme-specific and customize the look and feel of a theme 115 | # further. For a list of options available for each theme, see the 116 | # documentation. 117 | #html_theme_options = {} 118 | 119 | # Add any paths that contain custom themes here, relative to this directory. 120 | #html_theme_path = [] 121 | 122 | # The name for this set of Sphinx documents. If None, it defaults to 123 | # " v documentation". 124 | #html_title = None 125 | 126 | # A shorter title for the navigation bar. Default is the same as html_title. 127 | #html_short_title = None 128 | 129 | # The name of an image file (relative to this directory) to place at the top 130 | # of the sidebar. 131 | #html_logo = None 132 | 133 | # The name of an image file (within the static path) to use as favicon of the 134 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 135 | # pixels large. 136 | #html_favicon = None 137 | 138 | # Add any paths that contain custom static files (such as style sheets) here, 139 | # relative to this directory. They are copied after the builtin static files, 140 | # so a file named "default.css" will overwrite the builtin "default.css". 141 | #html_static_path = ['_static'] 142 | 143 | # Add any extra paths that contain custom files (such as robots.txt or 144 | # .htaccess) here, relative to this directory. These files are copied 145 | # directly to the root of the documentation. 146 | #html_extra_path = [] 147 | 148 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 149 | # using the given strftime format. 150 | #html_last_updated_fmt = '%b %d, %Y' 151 | 152 | # If true, SmartyPants will be used to convert quotes and dashes to 153 | # typographically correct entities. 154 | #html_use_smartypants = True 155 | 156 | # Custom sidebar templates, maps document names to template names. 157 | #html_sidebars = {} 158 | 159 | # Additional templates that should be rendered to pages, maps page names to 160 | # template names. 161 | #html_additional_pages = {} 162 | 163 | # If false, no module index is generated. 164 | #html_domain_indices = True 165 | 166 | # If false, no index is generated. 167 | #html_use_index = True 168 | 169 | # If true, the index is split into individual pages for each letter. 170 | #html_split_index = False 171 | 172 | # If true, links to the reST sources are added to the pages. 173 | #html_show_sourcelink = True 174 | 175 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 176 | #html_show_sphinx = True 177 | 178 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 179 | #html_show_copyright = True 180 | 181 | # If true, an OpenSearch description file will be output, and all pages will 182 | # contain a tag referring to it. The value of this option must be the 183 | # base URL from which the finished HTML is served. 184 | #html_use_opensearch = '' 185 | 186 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 187 | #html_file_suffix = None 188 | 189 | # Language to be used for generating the HTML full-text search index. 190 | # Sphinx supports the following languages: 191 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 192 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 193 | #html_search_language = 'en' 194 | 195 | # A dictionary with options for the search language support, empty by default. 196 | # Now only 'ja' uses this config value 197 | #html_search_options = {'type': 'default'} 198 | 199 | # The name of a javascript file (relative to the configuration directory) that 200 | # implements a search results scorer. If empty, the default will be used. 201 | #html_search_scorer = 'scorer.js' 202 | 203 | # Output file base name for HTML help builder. 204 | htmlhelp_basename = 'django-pgcrypto-expressionsdoc' 205 | 206 | # -- Options for LaTeX output --------------------------------------------- 207 | 208 | latex_elements = { 209 | # The paper size ('letterpaper' or 'a4paper'). 210 | #'papersize': 'letterpaper', 211 | 212 | # The font size ('10pt', '11pt' or '12pt'). 213 | #'pointsize': '10pt', 214 | 215 | # Additional stuff for the LaTeX preamble. 216 | #'preamble': '', 217 | 218 | # Latex figure (float) alignment 219 | #'figure_align': 'htbp', 220 | } 221 | 222 | # Grouping the document tree into LaTeX files. List of tuples 223 | # (source start file, target name, title, 224 | # author, documentclass [howto, manual, or own class]). 225 | latex_documents = [ 226 | (master_doc, 'django-pgcrypto-expressions.tex', 'django-pgcrypto-expressions Documentation', 227 | 'Carl Meyer', 'manual'), 228 | ] 229 | 230 | # The name of an image file (relative to this directory) to place at the top of 231 | # the title page. 232 | #latex_logo = None 233 | 234 | # For "manual" documents, if this is true, then toplevel headings are parts, 235 | # not chapters. 236 | #latex_use_parts = False 237 | 238 | # If true, show page references after internal links. 239 | #latex_show_pagerefs = False 240 | 241 | # If true, show URL addresses after external links. 242 | #latex_show_urls = False 243 | 244 | # Documents to append as an appendix to all manuals. 245 | #latex_appendices = [] 246 | 247 | # If false, no module index is generated. 248 | #latex_domain_indices = True 249 | 250 | 251 | # -- Options for manual page output --------------------------------------- 252 | 253 | # One entry per manual page. List of tuples 254 | # (source start file, name, description, authors, manual section). 255 | man_pages = [ 256 | (master_doc, 'django-pgcrypto-expressions', 'django-pgcrypto-expressions Documentation', 257 | [author], 1) 258 | ] 259 | 260 | # If true, show URL addresses after external links. 261 | #man_show_urls = False 262 | 263 | 264 | # -- Options for Texinfo output ------------------------------------------- 265 | 266 | # Grouping the document tree into Texinfo files. List of tuples 267 | # (source start file, target name, title, author, 268 | # dir menu entry, description, category) 269 | texinfo_documents = [ 270 | (master_doc, 'django-pgcrypto-expressions', 'django-pgcrypto-expressions Documentation', 271 | author, 'django-pgcrypto-expressions', 'One line description of project.', 272 | 'Miscellaneous'), 273 | ] 274 | 275 | # Documents to append as an appendix to all manuals. 276 | #texinfo_appendices = [] 277 | 278 | # If false, no module index is generated. 279 | #texinfo_domain_indices = True 280 | 281 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 282 | #texinfo_show_urls = 'footnote' 283 | 284 | # If true, do not generate a @detailmenu in the "Top" node's menu. 285 | #texinfo_no_detailmenu = False 286 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to django-pgcrypto-expressions! 2 | ======================================= 3 | 4 | `pgcrypto`_ for Django models. 5 | 6 | .. _pgcrypto: http://www.postgresql.org/docs/9.4/static/pgcrypto.html 7 | 8 | 9 | Prerequisites 10 | ------------- 11 | 12 | ``django-pgcrypto-expressions`` supports `Django`_ 1.8.2 and later on Python 13 | 2.7, 3.4, pypy, and pypy3. PostgreSQL is required. 14 | 15 | .. _Django: http://www.djangoproject.com/ 16 | 17 | 18 | Installation 19 | ------------ 20 | 21 | ``django-pgcrypto-expressions`` is available on `PyPI`_. Install it with:: 22 | 23 | pip install django-pgcrypto-expressions 24 | 25 | .. _PyPI: https://pypi.python.org/pypi/django-pgcrypto-expressions/ 26 | 27 | 28 | Setup 29 | ----- 30 | 31 | Your database must have the `pgcrypto`_ extension installed. You can install it 32 | by running ``CREATE EXTENSION pgcrypto;``. 33 | 34 | 35 | Usage 36 | ----- 37 | 38 | Just import and use the included field classes in your models:: 39 | 40 | from django.db import models 41 | from pgcrypto_expressions.fields import EncryptedTextField 42 | 43 | 44 | class MyModel(models.Model): 45 | name = EncryptedTextField() 46 | 47 | You can assign values to and read values from the ``name`` field as usual, but 48 | the values will automatically be encrypted using pgcrypto's ``pgp_sym_encrypt`` 49 | function when you save it, and decrypted using ``pgp_sym_decrypt`` when you 50 | load it from the database. 51 | 52 | 53 | Field types 54 | ~~~~~~~~~~~ 55 | 56 | Several other field classes are included: ``EncryptedCharField``, 57 | ``EncryptedEmailField``, ``EncryptedIntegerField``, ``EncryptedDateField``, and 58 | ``EncryptedDateTimeField``. All field classes accept the same arguments as 59 | their non-encrypted versions. 60 | 61 | To create an encrypted version of some other custom field class, inherit from 62 | both ``EncryptedField`` and the other field class:: 63 | 64 | from pgcrypto_expressions.fields import EncryptedField 65 | from somewhere import MyField 66 | 67 | class MyEncryptedField(EncryptedField, MyField): 68 | pass 69 | 70 | 71 | Encryption Key 72 | -------------- 73 | 74 | By default your ``SECRET_KEY`` setting is used as the encryption and decryption 75 | key. You can override this by setting a ``PGCRYPTO_KEY`` setting. 76 | 77 | Alternatively, if you are using multiple databases, you can specify a 78 | ``PGCRYPTO_KEY`` per database in your ``DATABASES`` setting. For example:: 79 | 80 | DATABASES = { 81 | 'default': { 82 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 83 | 'PGCRYPTO_KEY': 'super_secret_key', 84 | ... 85 | }, 86 | 'secondary': { 87 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 88 | 'PGCRYPTO_KEY': 'totally_different_secret_key', 89 | ... 90 | }, 91 | } 92 | 93 | 94 | .. warning:: 95 | 96 | Since encryption is performed on the database server, your encryption key is 97 | sent to the database server with each query involving an encrypted field. In 98 | order to protect your key, you should only connect to your database with a 99 | TLS-protected connection. It is possible that your key could be exposed to 100 | an attacker with access to the database server via the ``pg_stat_activity`` 101 | table or query logs. 102 | 103 | For an encrypted-fields solution that encrypts and decrypts on the 104 | application side to avoid this problem, see `django-fernet-fields`_. 105 | 106 | .. _django-fernet-fields: https://github.com/orcasgit/django-fernet-fields 107 | 108 | 109 | Indexing, constraints, lookups, and ordering 110 | -------------------------------------------- 111 | 112 | One advantage of encrypting and decrypting within the database is that 113 | encrypted fields may still be used in any type of lookup or database 114 | expression, and queries may be ordered by an encrypted field. 115 | 116 | However, indexing an encrypted field is not possible without storing the 117 | decryption key in the index expression (defeating the value of the encryption), 118 | so while lookups can be made against encrypted fields, those lookups or 119 | orderings cannot be indexed, meaning their performance will degrade with the 120 | size of the table. 121 | 122 | Similarly, unique or check constraints can't be applied to encrypted fields. 123 | 124 | 125 | Contributing 126 | ------------ 127 | 128 | See the `contributing docs`_. 129 | 130 | .. _contributing docs: https://github.com/orcasgit/django-pgcrypto-expressions/blob/master/CONTRIBUTING.rst 131 | -------------------------------------------------------------------------------- /pgcrypto_expressions/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3a1' 2 | -------------------------------------------------------------------------------- /pgcrypto_expressions/fields.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | from django.db import models 4 | from django.db.models.expressions import Col 5 | from django.utils.functional import cached_property 6 | 7 | 8 | class ByteArrayField(models.Field): 9 | """A simple field type for the Postgres bytea type. 10 | 11 | Represents values as bytearray instances in Python. 12 | 13 | """ 14 | def db_type(self, connection): 15 | return 'bytea' 16 | 17 | def from_db_value(self, value, expression, connection, context): 18 | if value is not None: 19 | return bytearray(value) 20 | 21 | 22 | class EncryptedField(models.Field): 23 | """A field mixin to encrypt any field type using pgcrypto. 24 | 25 | """ 26 | encrypt_sql_template = "pgp_sym_encrypt(%s::text, '{key}')" 27 | decrypt_sql_template = "pgp_sym_decrypt({sql}, '{key}')::{dbtype}" 28 | 29 | def __init__(self, *args, **kwargs): 30 | if kwargs.get('primary_key'): 31 | raise ImproperlyConfigured( 32 | "EncryptedField does not support primary key fields." 33 | ) 34 | if kwargs.get('unique'): 35 | raise ImproperlyConfigured( 36 | "EncryptedField does not support unique fields." 37 | ) 38 | if kwargs.get('db_index'): 39 | raise ImproperlyConfigured( 40 | "EncryptedField does not support indexing fields." 41 | ) 42 | self.key = getattr(settings, 'PGCRYPTO_KEY', settings.SECRET_KEY) 43 | super(EncryptedField, self).__init__(*args, **kwargs) 44 | 45 | def db_type(self, connection): 46 | return 'bytea' 47 | 48 | def _get_encryption_key(self, connection): 49 | if 'PGCRYPTO_KEY' in connection.settings_dict: 50 | key = connection.settings_dict['PGCRYPTO_KEY'] 51 | else: 52 | key = getattr(settings, 'PGCRYPTO_KEY', settings.SECRET_KEY) 53 | # Escape any percent symbols in the key, to avoid them being 54 | # interpreted as extra substitution placeholders later on. 55 | key = key.replace('%', '%%') 56 | return key 57 | 58 | def _get_base_db_type(self, connection): 59 | return super(EncryptedField, self).db_type(connection) 60 | 61 | def get_placeholder(self, value, compiler, connection): 62 | key = self._get_encryption_key(connection) 63 | return self.encrypt_sql_template.format(key=key) 64 | 65 | def get_col(self, alias, output_field=None): 66 | if output_field is None: 67 | output_field = self 68 | if alias != self.model._meta.db_table or output_field != self: 69 | return DecryptedCol(alias, self, self.decrypt_sql_template, 70 | output_field) 71 | else: 72 | return self.cached_col 73 | 74 | @cached_property 75 | def cached_col(self): 76 | return DecryptedCol( 77 | self.model._meta.db_table, 78 | self, 79 | self.decrypt_sql_template, 80 | ) 81 | 82 | 83 | class DecryptedCol(Col): 84 | def __init__(self, alias, target, decrypt_sql_template, output_field=None): 85 | self.decrypt_sql_template = decrypt_sql_template 86 | self.target = target 87 | super(DecryptedCol, self).__init__(alias, target, output_field) 88 | 89 | def as_sql(self, compiler, connection): 90 | sql, params = super(DecryptedCol, self).as_sql(compiler, connection) 91 | decrypt_sql = self.decrypt_sql_template.format( 92 | key=self.target._get_encryption_key(connection), 93 | dbtype=self.target._get_base_db_type(connection), 94 | sql=sql 95 | ) 96 | return decrypt_sql, params 97 | 98 | 99 | class EncryptedTextField(EncryptedField, models.TextField): 100 | pass 101 | 102 | 103 | class EncryptedCharField(EncryptedField, models.CharField): 104 | pass 105 | 106 | 107 | class EncryptedEmailField(EncryptedField, models.EmailField): 108 | pass 109 | 110 | 111 | class EncryptedIntegerField(EncryptedField, models.IntegerField): 112 | pass 113 | 114 | 115 | class EncryptedDateField(EncryptedField, models.DateField): 116 | pass 117 | 118 | 119 | class EncryptedDateTimeField(EncryptedField, models.DateTimeField): 120 | pass 121 | -------------------------------------------------------------------------------- /pgcrypto_expressions/funcs.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class PgpSymEncrypt(models.Func): 5 | """The pgp_sym_encrypt function from pgcrypto. 6 | 7 | Takes two arguments: a text value to be encrypted, and the encryption 8 | key. Returns a bytea. 9 | 10 | """ 11 | function = 'pgp_sym_encrypt' 12 | 13 | 14 | class PgpSymDecrypt(models.Func): 15 | """The pgp_sym_decrypt function from pgcrypto. 16 | 17 | Takes two arguments: a bytea value to be decrypted, and the encryption 18 | key. Returns text. 19 | 20 | """ 21 | function = 'pgp_sym_decrypt' 22 | -------------------------------------------------------------------------------- /pgcrypto_expressions/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orcasgit/django-pgcrypto-expressions/1f27c8d0beeba7193f220458c33573ba77e9f9b1/pgcrypto_expressions/test/__init__.py -------------------------------------------------------------------------------- /pgcrypto_expressions/test/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from pgcrypto_expressions import fields 4 | 5 | 6 | class ByteArrayModel(models.Model): 7 | content = fields.ByteArrayField(null=True) 8 | 9 | 10 | class EncryptedText(models.Model): 11 | value = fields.EncryptedTextField(default='hey') 12 | 13 | 14 | class RelatedText(models.Model): 15 | related = models.ForeignKey(EncryptedText) 16 | related_again = models.ForeignKey(EncryptedText, null=True) 17 | 18 | 19 | class EncryptedChar(models.Model): 20 | value = fields.EncryptedCharField(max_length=25) 21 | 22 | 23 | class RelatedChar(models.Model): 24 | related = models.ForeignKey(EncryptedChar) 25 | related_again = models.ForeignKey(EncryptedChar, null=True) 26 | 27 | 28 | class EncryptedEmail(models.Model): 29 | value = fields.EncryptedEmailField(default='hey') 30 | 31 | 32 | class RelatedEmail(models.Model): 33 | related = models.ForeignKey(EncryptedEmail) 34 | related_again = models.ForeignKey(EncryptedEmail, null=True) 35 | 36 | 37 | class EncryptedInt(models.Model): 38 | value = fields.EncryptedIntegerField(null=True) 39 | 40 | 41 | class RelatedInt(models.Model): 42 | related = models.ForeignKey(EncryptedInt) 43 | related_again = models.ForeignKey(EncryptedInt, null=True) 44 | 45 | 46 | class EncryptedDate(models.Model): 47 | value = fields.EncryptedDateField() 48 | 49 | 50 | class RelatedDate(models.Model): 51 | related = models.ForeignKey(EncryptedDate) 52 | related_again = models.ForeignKey(EncryptedDate, null=True) 53 | 54 | 55 | class EncryptedDateTime(models.Model): 56 | value = fields.EncryptedDateTimeField() 57 | 58 | 59 | class RelatedDateTime(models.Model): 60 | related = models.ForeignKey(EncryptedDateTime) 61 | related_again = models.ForeignKey(EncryptedDateTime, null=True) 62 | -------------------------------------------------------------------------------- /pgcrypto_expressions/test/settings.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | if platform.python_implementation() == 'PyPy': 4 | from psycopg2cffi import compat 5 | compat.register() 6 | 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 10 | 'NAME': 'djpgcetest', 11 | 'TEST': { 12 | 'NAME': 'djpgcetest', 13 | }, 14 | }, 15 | 'secondary': { 16 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 17 | 'NAME': 'djpgcetest_two', 18 | 'TEST': { 19 | 'NAME': 'djpgcetest_two', 20 | }, 21 | 'PGCRYPTO_KEY': 'secondary_key', 22 | }, 23 | } 24 | 25 | INSTALLED_APPS = [ 26 | 'pgcrypto_expressions.test' 27 | ] 28 | 29 | # Ensure that everything works even with a percent sign in the secret 30 | SECRET_KEY = 'sec%ret' 31 | 32 | SILENCED_SYSTEM_CHECKS = ['1_7.W001'] 33 | -------------------------------------------------------------------------------- /pgcrypto_expressions/test/test_fields.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | from django.core.exceptions import ImproperlyConfigured 3 | from django.db import connection, connections 4 | from django.db.models.sql.compiler import SQLInsertCompiler 5 | from django.db.models.sql.query import Query 6 | 7 | import pytest 8 | 9 | from pgcrypto_expressions import fields 10 | from . import models, utils 11 | 12 | 13 | class TestByteArrayField(object): 14 | def test_store_and_retrieve_bytearray(self, db): 15 | """Can store and retrieve data as bytearray.""" 16 | data = bytearray([27, 23, 29, 33]) 17 | models.ByteArrayModel.objects.create(content=data) 18 | found = models.ByteArrayModel.objects.get() 19 | 20 | assert found.content == data 21 | 22 | def test_store_null(self, db): 23 | """Conversion from db data doesn't choke on null/None.""" 24 | models.ByteArrayModel.objects.create(content=None) 25 | found = models.ByteArrayModel.objects.get() 26 | 27 | assert found.content is None 28 | 29 | 30 | class TestEncryptedField(object): 31 | def test_name(self): 32 | f = fields.EncryptedTextField(name='field') 33 | 34 | assert f.name == 'field' 35 | 36 | def test_verbose_name(self): 37 | f = fields.EncryptedTextField("The Field") 38 | 39 | assert f.verbose_name == "The Field" 40 | 41 | def test_primary_key_not_allowed(self): 42 | with pytest.raises(ImproperlyConfigured): 43 | fields.EncryptedIntegerField(primary_key=True) 44 | 45 | def test_unique_not_allowed(self): 46 | with pytest.raises(ImproperlyConfigured): 47 | fields.EncryptedIntegerField(unique=True) 48 | 49 | def test_db_index_not_allowed(self): 50 | with pytest.raises(ImproperlyConfigured): 51 | fields.EncryptedIntegerField(db_index=True) 52 | 53 | def test_deconstruct(self): 54 | f = fields.EncryptedTextField() 55 | 56 | assert 'key' not in f.deconstruct()[3] 57 | 58 | def test_PGCRYPTO_KEY_setting(self, settings): 59 | settings.PGCRYPTO_KEY = 'other' 60 | f = fields.EncryptedTextField() 61 | placeholder = f.get_placeholder('test', None, connection) 62 | 63 | assert 'other' in placeholder 64 | 65 | def test_PGCRYPTO_KEY_setting_per_database_encrypt(self, db): 66 | conn = connections['secondary'] 67 | f = fields.EncryptedTextField() 68 | placeholder = f.get_placeholder('test', None, conn) 69 | 70 | assert 'secondary_key' in placeholder 71 | 72 | 73 | RELATED = { 74 | models.EncryptedText: models.RelatedText, 75 | models.EncryptedChar: models.RelatedChar, 76 | models.EncryptedEmail: models.RelatedEmail, 77 | models.EncryptedInt: models.RelatedInt, 78 | models.EncryptedDate: models.RelatedDate, 79 | models.EncryptedDateTime: models.RelatedDateTime, 80 | } 81 | 82 | 83 | @pytest.mark.parametrize( 84 | 'model,vals', 85 | [ 86 | (models.EncryptedText, ('foo', 'bar')), 87 | (models.EncryptedChar, ('one', 'two')), 88 | (models.EncryptedEmail, ('a@example.com', 'b@example.com')), 89 | (models.EncryptedInt, (1, 2)), 90 | (models.EncryptedDate, (date(2015, 2, 5), date(2015, 2, 8))), 91 | ( 92 | models.EncryptedDateTime, 93 | (datetime(2015, 2, 5, 3), datetime(2015, 2, 8, 4))), 94 | ], 95 | ) 96 | class TestEncryptedFieldQueries(object): 97 | def test_insert(self, db, model, vals): 98 | """Data stored in DB is actually encrypted.""" 99 | field = model._meta.get_field('value') 100 | model.objects.create(value=vals[0]) 101 | data = utils.decrypt_column_values( 102 | model, 'value', 'sec%ret') 103 | 104 | assert list(map(field.to_python, data)) == [vals[0]] 105 | 106 | def test_insert_and_select(self, db, model, vals): 107 | """Data round-trips through insert and select.""" 108 | model.objects.create(value=vals[0]) 109 | found = model.objects.get() 110 | 111 | assert found.value == vals[0] 112 | 113 | def test_update_and_select(self, db, model, vals): 114 | """Data round-trips through update and select.""" 115 | model.objects.create(value=vals[0]) 116 | model.objects.update(value=vals[1]) 117 | found = model.objects.get() 118 | 119 | assert found.value == vals[1] 120 | 121 | def test_exact_lookup(self, db, model, vals): 122 | """Can do exact lookups against encrypted fields.""" 123 | model.objects.create(value=vals[0]) 124 | found = model.objects.get(value=vals[0]) 125 | 126 | assert found.value == vals[0] 127 | 128 | def test_in_lookup(self, db, model, vals): 129 | """Can do __in lookups against encrypted fields.""" 130 | model.objects.create(value=vals[0]) 131 | found = model.objects.get(value__in=vals) 132 | 133 | assert found.value == vals[0] 134 | 135 | def test_select_related(self, db, model, vals): 136 | """Can select related models with encrypted fields.""" 137 | obj = model.objects.create(value=vals[0]) 138 | related_model = RELATED[model] 139 | related_model.objects.create(related=obj) 140 | found = related_model.objects.select_related( 141 | 'related' 142 | ).get() 143 | 144 | assert found.related.value == vals[0] 145 | 146 | def test_related_lookup(self, db, model, vals): 147 | """Can do joined lookups against encrypted fields.""" 148 | obj = model.objects.create(value=vals[0]) 149 | related_model = RELATED[model] 150 | related_model.objects.create(related=obj) 151 | found = related_model.objects.get(related__value=vals[0]) 152 | 153 | assert found.related.value == vals[0] 154 | 155 | def test_double_select_related(self, db, model, vals): 156 | """Can select related the same model with an encrypted field twice.""" 157 | obj = model.objects.create(value=vals[0]) 158 | obj2 = model.objects.create(value=vals[1]) 159 | related_model = RELATED[model] 160 | related_model.objects.create(related=obj, related_again=obj2) 161 | found = related_model.objects.select_related( 162 | 'related', 'related_again', 163 | ).get() 164 | 165 | assert found.related.value == vals[0] 166 | assert found.related_again.value == vals[1] 167 | 168 | def test_PGCRYPT_KEY_setting_per_database_decrypt(self, db, model, vals): 169 | conn = connections['secondary'] 170 | obj = model(value=vals[0]) 171 | field = obj._meta.get_field('value') 172 | col = field.get_col(obj._meta.db_table) 173 | query = Query(model) 174 | compiler = SQLInsertCompiler(query, conn, 'secondary') 175 | sql, params = col.as_sql(compiler, conn) 176 | 177 | assert 'secondary_key' in sql 178 | 179 | 180 | class TestEncryptedTextField(object): 181 | def test_contains_lookup(self, db): 182 | """Can do __contains lookups against encrypted fields.""" 183 | models.EncryptedText.objects.create(value='foobar') 184 | found = models.EncryptedText.objects.get(value__contains='oob') 185 | 186 | assert found.value == 'foobar' 187 | 188 | def test_default(self, db): 189 | """Field default values are respected.""" 190 | models.EncryptedText.objects.create() 191 | found = models.EncryptedText.objects.get() 192 | 193 | assert found.value == 'hey' 194 | 195 | 196 | class TestEncryptedIntegerField(object): 197 | def test_gt_lookup(self, db): 198 | """Can do __gt lookups against encrypted fields.""" 199 | models.EncryptedInt.objects.create(value=4) 200 | found = models.EncryptedInt.objects.get(value__gt=3) 201 | 202 | assert found.value == 4 203 | 204 | def test_range_lookup(self, db): 205 | """Can do __range lookups against encrypted fields.""" 206 | models.EncryptedInt.objects.create(value=4) 207 | found = models.EncryptedInt.objects.get(value__range=[3, 5]) 208 | 209 | assert found.value == 4 210 | 211 | def test_nullable(self, db): 212 | """Encrypted field can be nullable.""" 213 | models.EncryptedInt.objects.create(value=None) 214 | found = models.EncryptedInt.objects.get() 215 | 216 | assert found.value is None 217 | 218 | def test_ordering(self, db): 219 | """Can order by an encrypted field.""" 220 | models.EncryptedInt.objects.create(value=5) 221 | models.EncryptedInt.objects.create(value=2) 222 | 223 | found = models.EncryptedInt.objects.order_by('value') 224 | 225 | assert [f.value for f in found] == [2, 5] 226 | -------------------------------------------------------------------------------- /pgcrypto_expressions/test/test_funcs.py: -------------------------------------------------------------------------------- 1 | from django.db.models import TextField, Value as V 2 | 3 | from pgcrypto_expressions import funcs 4 | from .models import ByteArrayModel 5 | from . import utils 6 | 7 | 8 | class TestPgpSymEncrypt(object): 9 | def test_encrypt(self, db): 10 | ByteArrayModel.objects.create() 11 | ByteArrayModel.objects.update( 12 | content=funcs.PgpSymEncrypt(V('hello'), V('secret'))) 13 | data = utils.decrypt_column_values( 14 | ByteArrayModel, 'content', 'secret') 15 | 16 | assert data == ["hello"] 17 | 18 | def test_decrypt(self, db): 19 | ByteArrayModel.objects.create() 20 | ByteArrayModel.objects.update( 21 | content=funcs.PgpSymEncrypt(V('hello'), V('secret'))) 22 | found = ByteArrayModel.objects.annotate( 23 | decrypted=funcs.PgpSymDecrypt( 24 | 'content', V('secret'), output_field=TextField()) 25 | ).get() 26 | assert found.decrypted == "hello" 27 | -------------------------------------------------------------------------------- /pgcrypto_expressions/test/utils.py: -------------------------------------------------------------------------------- 1 | from django.db import connection 2 | 3 | 4 | def decrypt_column_values(model, field_name, secret): 5 | """Return decrypted column values as list, direct from db.""" 6 | with connection.cursor() as cur: 7 | cur.execute( 8 | "SELECT pgp_sym_decrypt(%(field_name)s, '%(secret)s') " 9 | "FROM %(table_name)s" % { 10 | 'field_name': field_name, 11 | 'secret': secret, 12 | 'table_name': model._meta.db_table 13 | } 14 | ) 15 | return [row[0] for row in cur.fetchall()] 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements for running django-pgcrypto-expressions tests 2 | 3 | Django>=1.8.2 4 | 5 | tox>=2.0.1 6 | pytest-django==2.8.0 7 | pytest==2.7.1 8 | coverage==4.0.3 9 | 10 | psycopg2==2.6 11 | psycopg2cffi==2.6.1 12 | 13 | Sphinx==1.3.1 14 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This script exists so this dir is on sys.path when running pytest in tox. 3 | import pytest 4 | import os 5 | import sys 6 | 7 | os.environ.setdefault( 8 | 'DJANGO_SETTINGS_MODULE', 'pgcrypto_expressions.test.settings') 9 | 10 | sys.exit(pytest.main()) 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os.path import join 2 | from setuptools import setup, find_packages 3 | 4 | 5 | long_description = ( 6 | open('README.rst').read() + open('CHANGES.rst').read()) 7 | 8 | 9 | def get_version(): 10 | with open(join('pgcrypto_expressions', '__init__.py')) as f: 11 | for line in f: 12 | if line.startswith('__version__ ='): 13 | return line.split('=')[1].strip().strip('"\'') 14 | 15 | 16 | setup( 17 | name='django-pgcrypto-expressions', 18 | version=get_version(), 19 | description=( 20 | 'PGCrypto support for Django 1.8+' 21 | ), 22 | long_description=long_description, 23 | author='ORCAS, Inc', 24 | author_email='orcastech@orcasinc.com', 25 | url='https://github.com/orcasgit/django-pgcrypto-expressions/', 26 | packages=find_packages(), 27 | install_requires=['Django>=1.8.2'], 28 | classifiers=[ 29 | 'Environment :: Web Environment', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: BSD License', 32 | 'Operating System :: OS Independent', 33 | 'Programming Language :: Python', 34 | 'Programming Language :: Python :: 2', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.2', 38 | 'Programming Language :: Python :: 3.3', 39 | 'Programming Language :: Python :: 3.4', 40 | 'Programming Language :: Python :: Implementation :: CPython', 41 | 'Programming Language :: Python :: Implementation :: PyPy', 42 | 'Framework :: Django', 43 | ], 44 | zip_safe=False, 45 | ) 46 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{27,34,py,py3}-django18, 4 | py{27,34,py}-django19, 5 | flake8, 6 | docs 7 | 8 | 9 | [testenv] 10 | deps = 11 | pytest-django==2.8.0 12 | pytest==2.7.1 13 | py==1.4.27 14 | coverage==4.0.3 15 | django18: Django>=1.8,<1.9 16 | django19: Django>=1.9,<1.10 17 | py{26,27,34}: psycopg2==2.6 18 | {pypy,pypy3}: psycopg2cffi==2.6.1 19 | commands = 20 | coverage run -a runtests.py pgcrypto_expressions/test --tb short 21 | 22 | [testenv:flake8] 23 | deps = flake8 24 | changedir = {toxinidir} 25 | commands = flake8 . 26 | 27 | [testenv:docs] 28 | deps = Sphinx==1.3.1 29 | changedir = {toxinidir}/doc 30 | commands = 31 | sphinx-build -aEWq -b html . _build/html 32 | 33 | [flake8] 34 | exclude = .tox,.git,__pycache__,doc/conf.py 35 | --------------------------------------------------------------------------------