Repository: incuna/django-pgcrypto-fields Branch: master Commit: 68d60e173e22 Files: 34 Total size: 109.4 KB Directory structure: gitextract_iiwlznvz/ ├── .gitignore ├── .travis.yml ├── AUTHORS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── pgcrypto/ │ ├── __init__.py │ ├── fields.py │ ├── lookups.py │ ├── migrations/ │ │ ├── 0001_add_pgcrypto_extension.py │ │ └── __init__.py │ ├── mixins.py │ └── models.py ├── requirements.txt ├── requirements_dev.txt ├── setup.cfg ├── setup.py └── tests/ ├── __init__.py ├── dbrouters.py ├── default/ │ └── __init__.py ├── diff_keys/ │ ├── __init__.py │ └── models.py ├── factories.py ├── forms.py ├── keys/ │ ├── README.md │ ├── private.key │ ├── private_diff.key │ ├── public.key │ └── public_diff.key ├── models.py ├── run.py └── test_fields.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ .idea/ # Django stuff: *.log local_settings.py # virtualenv .venv/ venv/ ENV/ ================================================ FILE: .travis.yml ================================================ language: python sudo: required dist: xenial python: - 3.6 - 3.7 - 3.8 - 3.9-dev script: make test-coveralls notifications: email: false services: - postgresql before_script: - psql -c 'CREATE DATABASE pgcrypto_fields' -U postgres - psql -c 'CREATE DATABASE pgcrypto_fields_diff' -U postgres install: - pip install -e . - pip install -r requirements_dev.txt - pip install $DJANGO env: matrix: - DJANGO='django>=2.2,<2.3' - DJANGO='django>=3.0,<3.1' - DJANGO='django>=3.1,<3.2' - DJANGO='--upgrade --pre django' matrix: fast_finish: true allow_failures: - env: DJANGO='--upgrade --pre django' - python: 3.9-dev ================================================ FILE: AUTHORS.md ================================================ # Credits ## Development Lead * Charlie Denton * Kévin Etienne * Peter J. Farrell * Max Peterson ## Contributors * Esther Onfroy ================================================ FILE: CHANGELOG.md ================================================ # CHANGELOG ## Master (unreleased) * Added support for BooleanFields (#325) ## 2.6.0 * Added support for Django 3.1.x * Updated requirements_dev.txt * Dropped support for Python 3.5 * Dropped support for Django below 2.2.x LTS release * Added support for BigIntegerFields (#169) * Added documentation for migration existing data (#246) ## 2.5.2 * Added support for Django 3.x * Updated requirements_dev.txt ## 2.5.1 * Fixed regression in the definition of EmailPGPPublicKeyField (#77) * Removed dead code (remove_validators and RemoveMaxLengthValidatorMixin) * Updated requirements_dev.txt ## 2.5.0 * Added new DecimalFields for both public and symmetric key (#64) * Added new FloatFields for both public and symmetric key (#64) * Added new TimeFields for both public and symmetric key (#64) * Added support for different keys based on database (#67) ## 2.4.0 * Added auto-decryption of all encrypted fields including FK tables * Removed django-pgcrypto-fields `aggregates`, `PGPManager` and `PGPAdmin` as they are no longer needed * Added support for `get_or_create()` and `update_or_create()` (#27) * Added support for `get_by_natural_key()` (#23) * Added support for `only()` and `defer()` as they were not supported with `PGPManager` * Added support for `distinct()` (Django 2.1+ with workaround available for 2.0 and lower) * Separated out dev requirements from setup.py requirements * Updated packaging / setup.py to include long description * Added AUTHORS and updated CONTRIBUTING * Updated TravisCI to use Xenial to gain Python 3.7 in the matrix ## 2.3.1 * Added `__range` lookup for Date / DateTime fields (#59) * Remove compatibility for `Django 1.8, 1.9, and 1.10` (#62) * Improved `setup.py`: * check for Python 3.5+ * updated classifiers * Improved `make` file for release to use `twine` * Added additional shields to `README` * Updated Travis config to include Python 3.5 and 3.6 * Refactored lookups and mixins ## 2.3.0 * Invalid release, bump to 2.3.1 ## 2.2.0 * Merge `.coveragerc` into `setup.cfg` * Added `.gitignore` file * Updated out-dated requirements (latest versions of `Flake8` and `pycodestyle` are incompatible with each other) * Updated `README` with better explanations of the fields * Implemented DatePGPPublicKeyField and DateTimePGPPublicKeyField ## 2.1.1 * Added support for Django 2.x+ * Updated requirements for testing * Updated travis config with Python 3.6 and additional environments ## 2.1.0 Thanks to @peterfarrell: * Add support for `DatePGPSymmetricKeyField` and `DateTimePGPSymmetricKeyField` including support for serializing / deserializing django form fields. * Add support for auto decryption of symmetric key and public key fields via the PGPManager (and support for disabling it in the Django Admin via the PGPAdmin) ## 2.0.0 * Remove compatibility for `Django 1.7`. * Add compatibility for `Django 1.10`. * Add `Django 1.9` to the travis matrix. ## v1.0.1 * Exclude tests app from distributed package. ## v1.0.0 * Rename package from `pgcrypto_fields` to `pgcrypto`. ## v0.7.0 * Make `get_placeholder` accepts a new argument `compiler` * Fix buggy import to `Aggregate` **Note: these changes have been done for django > 1.8.0.** ## v0.6.4 * Remove `MaxLengthValidator` from email fields. ## v0.6.3 * Avoid setting `max_length` on PGP fields. ## v0.6.2 * Allow/check `NULL` values for: `TextDigestField`; `TextHMACField`; `EmailPGPPublicKeyField`; `IntegerPGPPublicKeyField`; `TextPGPPublicKeyField`; `EmailPGPSymmetricKeyField`. `IntegerPGPSymmetricKeyField`. `TextPGPSymmetricKeyField`. ## v0.6.1 * Fix `cast`ing bug when sending negative values to integer fields. ## v0.6.0 * Add `EmailPGPPublicKeyField` and `EmailPGPSymmetricKeyField`. ## v0.5.0 * Rename the following fields: `PGPPublicKeyField` to `TextPGPPublicKeyField`; `PGPSymmetricKeyField` to `TextPGPSymmetricKeyField`; `DigestField` to `TextDigestField`; `HMACField` to `TextHMACField`. * Add new integer fields: `IntegerPGPPublicKeyField`; `IntegerPGPSymmetricKeyField`. ## v0.4.0 * Make accessing decrypted value transparent. Fix bug when field had a string representation of `memoryview` for PGP and keyed hash fields. ## v0.3.1 * Fix `EncryptedProxyField` to select the correct item. ## v0.3.0 * Access `PGPPublicKeyField` and `PGPSymmetricKeySQL` decrypted values with field's proxy `_decrypted`. * Remove descriptor for field's name and raw value. ## v0.2.0 * Add hash based lookup for `DigestField` and `HMACField`. * Add `DigestField`, `HMACField`, `PGPPublicKeyAggregate`, `PGPSymmetricKeyAggregate`. ## v0.1.0 * Add decryption through an aggregate class. * Add encryption when inserting data to the database. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Django-PGCrypto-Fields Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: * Code patches and enhancements * Documentation improvements * Bug reports and patch reviews ## Types of Contributions ### Report Bugs Report bugs at https://github.com/incuna/django-pgcrypto-fields/issues If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. ### Fix Bugs Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it. ### Implement Features Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. ### Write Documentation django-pgcrypto-fields could always use more documentation, whether as part of the official django-pgcrypto-fields docs, in docstrings, or even on the web in blog posts, articles, and such. ### Submit Feedback The best way to send feedback is to file an issue at https://github.com/incuna/django-pgcrypto-fields/issues If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that contributions are welcome :-) ## Get Started! Ready to contribute? Here's how to set up `django-pgcrypto-fields` for local development. 1. Fork the `django-pgcrypto-fields` repo on GitHub. 2. Clone your fork locally: ```bash $ git clone git@github.com:your_name_here/pgcrypto.git ``` 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development: ```bash $ mkvirtualenv django-pgcrypto-fields $ cd django-pgcrypto-fields/ $ pip install -r requirements_dev.txt --upgrade ``` 4. Create a branch for local development: ```bash $ git checkout -b name-of-your-bugfix-or-feature ``` Now you can make your changes locally. 5. When you're done making changes, check that your changes pass flake8 and the tests: ```bash $ make test ``` To get flake8 and tox, just pip install them into your virtualenv. You will need a postgres database called `pgcrypto_fields` and `pgcrypto_fields_diff` on localhost without password authentication 6. Commit your changes and push your branch to GitHub: ```bash $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature ``` 7. Submit a pull request through the GitHub website. ### Pull Request Guidelines Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. 3. The pull request should work for Python 3.6, 3.7 and 3.8. Check https://travis-ci.org/incuna/django-pgcrypto-fields/pull_requests and make sure that the tests pass for all supported Python versions. ### Deploying A reminder for the maintainers on how to deploy. Make sure all your changes are committed (including an entry in CHANGELOG.md). Then run: ```bash $ make release ``` ================================================ FILE: LICENSE ================================================ Copyright (c) 2014 Incuna Ltd All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Makefile ================================================ .PHONY: clean-build lint help .DEFAULT_GOAL := help define BROWSER_PYSCRIPT import os, webbrowser, sys try: from urllib import pathname2url except: from urllib.request import pathname2url webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) endef export BROWSER_PYSCRIPT define PRINT_HELP_PYSCRIPT import re, sys for line in sys.stdin: match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) if match: target, help = match.groups() print("%-20s %s" % (target, help)) endef export PRINT_HELP_PYSCRIPT BROWSER := python -c "$$BROWSER_PYSCRIPT" define BROWSER_PYSCRIPT import os, webbrowser, sys try: from urllib import pathname2url except: from urllib.request import pathname2url webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) endef export BROWSER_PYSCRIPT define PRINT_HELP_PYSCRIPT import re, sys for line in sys.stdin: match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) if match: target, help = match.groups() print("%-20s %s" % (target, help)) endef export PRINT_HELP_PYSCRIPT BROWSER := python -c "$$BROWSER_PYSCRIPT" help: @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) lint: ## Check style with flake8 @flake8 . --exit-zero clean-build: ## Remove build artifacts rm -r -f dist/* rm -r -f build/* rm -fr htmlcov/ build: clean-build ## Builds source and wheel package python setup.py sdist bdist_wheel ls -l dist release: ## Package and upload a release twine upload dist/* test: clean-build lint ## Run tests quickly with the default Python ./tests/run.py test-coverage: ## Check code coverage quickly with the default Python coverage run ./tests/run.py coverage report -m test-coveralls: test-coverage ## Check code coverage with the default Python and Coveralls coveralls test-coverage-html: test-coverage ## Check code coverage quickly with the default Python and show report coverage html $(BROWSER) htmlcov/index.html ================================================ FILE: README.md ================================================ # django-pgcrypto-fields [![Latest Release](https://img.shields.io/pypi/v/django-pgcrypto-fields.svg)](https://pypi.org/pypi/django-pgcrypto-fields/) [![Python Versions](https://img.shields.io/pypi/pyversions/django-pgcrypto-fields.svg)](https://pypi.org/pypi/django-pgcrypto-fields/) [![Build Status](https://travis-ci.org/incuna/django-pgcrypto-fields.svg?branch=master)](https://travis-ci.org/incuna/django-pgcrypto-fields?branch=master) [![Requirements Status](https://requires.io/github/incuna/django-pgcrypto-fields/requirements.svg?branch=master)](https://requires.io/github/incuna/django-pgcrypto-fields/requirements/?branch=master) [![Updates](https://pyup.io/repos/github/incuna/django-pgcrypto-fields/shield.svg)](https://pyup.io/repos/github/incuna/django-pgcrypto-fields/) [![Coverage Status](https://coveralls.io/repos/github/incuna/django-pgcrypto-fields/badge.svg?branch=master)](https://coveralls.io/github/incuna/django-pgcrypto-fields?branch=master) `django-pgcrypto-fields` is a `Django` extension which relies upon `pgcrypto` to encrypt and decrypt data for fields. ## Requirements - postgres with `pgcrypto` - Supports Django 2.2.x, 3.0.x, 3.1.x and 3.2.x - Compatible with Python 3 only Last version of this library that supports `Django` 1.8.x, 1.9.x, 1.10.x was `django-pgcrypto-fields` 2.2.0. Last version of this library that supports `Django` 2.0.x and 2.1.x was was `django-pgcrypto-fields` 2.5.2. ## Installation ### Install package ```bash pip install django-pgcrypto-fields ``` ### Django settings Our library support different crypto keys for multiple databases by defining the keys in your `DATABASES` settings. In `settings.py`: ```python import os BASEDIR = os.path.dirname(os.path.dirname(__file__)) PUBLIC_PGP_KEY_PATH = os.path.abspath(os.path.join(BASEDIR, 'public.key')) PRIVATE_PGP_KEY_PATH = os.path.abspath(os.path.join(BASEDIR, 'private.key')) # Used by PGPPublicKeyField used by default if not specified by the db PUBLIC_PGP_KEY = open(PUBLIC_PGP_KEY_PATH).read() PRIVATE_PGP_KEY = open(PRIVATE_PGP_KEY_PATH).read() # Used by TextHMACField and PGPSymmetricKeyField if not specified by the db PGCRYPTO_KEY='ultrasecret' DIFF_PUBLIC_PGP_KEY_PATH = os.path.abspath( os.path.join(BASEDIR, 'tests/keys/public_diff.key') ) DIFF_PRIVATE_PGP_KEY_PATH = os.path.abspath( os.path.join(BASEDIR, 'tests/keys/private_diff.key') ) # And add 'pgcrypto' to `INSTALLED_APPS` to create the extension for # pgcrypto (in a migration). INSTALLED_APPS = ( 'pgcrypto', # Other installed apps ) DATABASES = { # This db will use the default keys above 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'pgcryto_fields', 'USER': 'pgcryto_fields', 'PASSWORD': 'xxxx', 'HOST': 'psql.test.com', 'PORT': 5432, 'OPTIONS': { 'sslmode': 'require', } }, 'diff_keys': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'pgcryto_fields_diff', 'USER': 'pgcryto_fields_diff', 'PASSWORD': 'xxxx', 'HOST': 'psqldiff.test.com', 'PORT': 5432, 'OPTIONS': { 'sslmode': 'require', }, 'PGCRYPTO_KEY': 'djangorocks', 'PUBLIC_PGP_KEY': open(DIFF_PUBLIC_PGP_KEY_PATH, 'r').read(), 'PRIVATE_PGP_KEY': open(DIFF_PRIVATE_PGP_KEY_PATH, 'r').read(), }, } ``` ### Generate GPG keys if using Public Key Encryption The public key is going to encrypt the message and the private key will be needed to decrypt the content. The following commands have been taken from the [pgcrypto documentation](http://www.postgresql.org/docs/devel/static/pgcrypto.html) (see Generating PGP Keys with GnuPG). Generating a public and a private key (The preferred key type is "DSA and Elgamal".): ```bash $ gpg --gen-key $ gpg --list-secret-keys /home/bob/.gnupg/secring.gpg --------------------------- sec 2048R/21 2014-10-23 uid Test Key ssb 2048R/42 2014-10-23 $ gpg -a --export 42 > public.key $ gpg -a --export-secret-keys 21 > private.key ``` #### Limitations This library currently does not support Public Key Encryption private keys that are password protected yet. See Issue #89 to help implement it. ### Upgrading to 2.4.0 from previous versions The 2.4.0 version of this library received a large rewrite in order to support auto-decryption when getting encrypted field data as well as the ability to filter on encrypted fields without using the old PGPCrypto aggregate functions available in previous versions. The following items in this library have been removed and therefore references in your application to these items need to be removed as well: * `managers.PGPManager` * `admin.PGPAdmin` * `aggregates.*` ## Fields `django-pgcrypto-fields` has 3 kinds of fields: - Hash based fields - Public Key (PGP) fields - Symmetric fields #### Hash Based Fields Supported hash based fields are: - `TextDigestField` - `TextHMACField` `TextDigestField` is hashed in the database using the `digest` pgcrypto function using the `sha512` algorithm. `TextHMACField` is hashed in the database using the `hmac` pgcrypto function using a key and the `sha512` algorithm. This is similar to the digest version however the hash can only be recalculated knowing the key. This prevents someone from altering the data and also changing the hash to match. #### Public Key Encryption Fields Supported PGP public key fields are: - `CharPGPPublicKeyField` - `EmailPGPPublicKeyField` - `TextPGPPublicKeyField` - `DatePGPPublicKeyField` - `DateTimePGPPublicKeyField` - `TimePGPPublicKeyField` - `IntegerPGPPublicKeyField` - `BigIntegerPGPPublicKeyField` - `DecimalPGPPublicKeyField` - `FloatPGPPublicKeyField` - `BooleanPGPPublicKeyField` Public key encryption creates a token generated with a public key to encrypt the data and a private key to decrypt it. Public and private keys can be set in settings with `PUBLIC_PGP_KEY` and `PRIVATE_PGP_KEY`. #### Symmetric Key Encryption Fields Supported PGP symmetric key fields are: - `CharPGPSymmetricKeyField` - `EmailPGPSymmetricKeyField` - `TextPGPSymmetricKeyField` - `DatePGPSymmetricKeyField` - `DateTimePGPSymmetricKeyField` - `TimePGPSymmetricKeyField` - `IntegerPGPSymmetricKeyField` - `BigIntegerPGPSymmetricKeyField` - `DecimalPGPSymmetricKeyField` - `FloatPGPSymmetricKeyField` - `BooleanPGPSymmetricKeyField` Encrypt and decrypt the data with `settings.PGCRYPTO_KEY` which acts like a password. ### Django Model Field Equivalents | Django Field | Public Key Field | Symmetric Key Field | |-----------------|-----------------------------|--------------------------------| | `CharField` | `CharPGPPublicKeyField` | `CharPGPSymmetricKeyField` | | `EmailField` | `EmailPGPPublicKeyField` | `EmailPGPSymmetricKeyField` | | `TextField` | `TextPGPPublicKeyField` | `TextPGPSymmetricKeyField` | | `DateField` | `DatePGPPublicKeyField` | `DatePGPSymmetricKeyField` | | `DateTimeField` | `DateTimePGPPublicKeyField` | `DateTimePGPSymmetricKeyField` | | `TimeField` | `TimePGPPublicKeyField` | `TimePGPSymmetricKeyField` | | `IntegerField` | `IntegerPGPPublicKeyField` | `IntegerPGPSymmetricKeyField` | | `BigIntegerField` | `BigIntegerPGPPublicKeyField` | `BigIntegerPGPSymmetricKeyField` | | `DecimalField` | `DecimalPGPPublicKeyField` | `DecimalPGPSymmetricKeyField` | | `FloatField` | `FloatPGPPublicKeyField` | `FloatPGPSymmetricKeyField` | | `BooleanField` | `BooleanPGPPublicKeyField` | `BooleanPGPSymmetricKeyField` | **Other Django model fields are not currently supported. Pull requests are welcomed.** ### Usage #### Model Definition ```python from django.db import models from pgcrypto import fields class MyModel(models.Model): digest_field = fields.TextDigestField() digest_with_original_field = fields.TextDigestField(original='pgp_sym_field') hmac_field = fields.TextHMACField() hmac_with_original_field = fields.TextHMACField(original='pgp_sym_field') email_pgp_pub_field = fields.EmailPGPPublicKeyField() integer_pgp_pub_field = fields.IntegerPGPPublicKeyField() pgp_pub_field = fields.TextPGPPublicKeyField() date_pgp_pub_field = fields.DatePGPPublicKeyField() datetime_pgp_pub_field = fields.DateTimePGPPublicKeyField() time_pgp_pub_field = fields.TimePGPPublicKeyField() decimal_pgp_pub_field = fields.DecimalPGPPublicKeyField() float_pgp_pub_field = fields.FloatPGPPublicKeyField() boolean_pgp_pub_field = fields.BooleanPGPPublicKeyField() email_pgp_sym_field = fields.EmailPGPSymmetricKeyField() integer_pgp_sym_field = fields.IntegerPGPSymmetricKeyField() pgp_sym_field = fields.TextPGPSymmetricKeyField() date_pgp_sym_field = fields.DatePGPSymmetricKeyField() datetime_pgp_sym_field = fields.DateTimePGPSymmetricKeyField() time_pgp_sym_field = fields.TimePGPSymmetricKeyField() decimal_pgp_sym_field = fields.DecimalPGPSymmetricKeyField() float_pgp_sym_field = fields.FloatPGPSymmetricKeyField() boolean_pgp_sym_field = fields.BooleanPGPSymmetricKeyField() ``` #### Encrypting Data is automatically encrypted when inserted into the database. Example: ``` >>> MyModel.objects.create(value='Value to be encrypted...') ``` Hash fields can have hashes auto updated if you use the `original` attribute. This attribute allows you to indicate another field name to base the hash value on. ```python from django.db import models from pgcrypto import fields class User(models.Model): first_name = fields.TextPGPSymmetricKeyField(max_length=20, verbose_name='First Name') first_name_hashed = fields.TextHMACField(original='first_name') ``` In the above example, if you specify the optional original attribute it would take the unencrypted value from the first_name model field as the input value to create the hash. If you did not specify an original attribute, the field would work as it does now and would remain backwards compatible. ##### PGP fields When accessing the field name attribute on a model instance we are getting the decrypted value. Example: ``` >>> # When using a PGP public key based encryption >>> my_model = MyModel.objects.get() >>> my_model.value 'Value decrypted' ``` Filtering encrypted values is now handled automatically as of 2.4.0. And `aggregate` methods are not longer supported and have been removed from the library. Also, auto-decryption is support for `select_related()` models. ```python from django.db import models from pgcrypto import fields class EncryptedFKModel(models.Model): fk_pgp_sym_field = fields.TextPGPSymmetricKeyField(blank=True, null=True) class EncryptedModel(models.Model): pgp_sym_field = fields.TextPGPSymmetricKeyField(blank=True, null=True) fk_model = models.ForeignKey( EncryptedFKModel, blank=True, null=True, on_delete=models.CASCADE ) ``` Example: ``` >>> import EncryptedModel >>> my_model = EncryptedModel.objects.get().select_releated('fk_model') >>> my_model.pgp_sym_field 'Value decrypted' >>> my_model.fk_model.fk_pgp_sym_field 'Value decrypted' ``` ##### Hash fields To filter hash based values we need to compare hashes. This is achieved by using a `__hash_of` lookup. Example: ``` >>> my_model = MyModel.objects.filter(digest_field__hash_of='value') [] >>> my_model = MyModel.objects.filter(hmac_field__hash_of='value') [] ``` ## Limitations ### Unique Indexes It is usually not possible to index a `bytea` column in the database as the value in the index exceeds the the pgsql's maximum length allowed for an index (8192 bytes). One solution is to create a digest message of the value that you want unique and apply the unique constraint to the digest. You can use the hash field ability to auto-create digest on the value of another field in the same model using the `original` argument. In the example below, a digest is created for unencrypted value that is in the `name` field when the model is saved or updated. A unique constraint exists on the name_digest so no two digests are allowed. Note well that bulk updates do NOT cause hashes to be updated. ```python from django.db import models from pgcrypto import fields class Product(models.Model): name_digest = fields.TextDigestField(original='name') name = fields.TextPGPSymmetricKeyField() class Meta: constraints = [ models.UniqueConstraint( fields=['name_digest', ], name='name_digest_unique' ) ] ``` ### `.distinct('encrypted_field_name')` Due to a missing feature in the Django ORM, using `distinct()` on an encrypted field does not work for Django 2.0.x and lower. The normal distinct works on Django 2.1.x and higher: ```python items = EncryptedFKModel.objects.filter( pgp_sym_field__startswith='P' ).only( 'id', 'pgp_sym_field', 'fk_model__fk_pgp_sym_field' ).distinct( 'pgp_sym_field' ) ``` Workaround for Django 2.0.x and lower: ```python from django.db import models items = EncryptedFKModel.objects.filter( pgp_sym_field__startswith='P' ).annotate( _distinct=models.F('pgp_sym_field') ).only( 'id', 'pgp_sym_field', 'fk_model__fk_pgp_sym_field' ).distinct( '_distinct' ) ``` This works because the annotated field is auto-decrypted by Django as a `F` field and that field is used in the `distinct()`. ### Migrating existing fields into PGCrypto Fields Migrating existing fields into PGCrypto Fields is not performed by this library. You will need to migrate the data in a forwards migration or other means. The only migration that is supported except to create/activate the pgcrypto extension in Postgres. Migrating data is complicated as there might be a few things to consider such as: * the shape of the data * validations/constrains done on the table/model/form and anywhere else The library has no way of doing all these guesses or to make all these decisions. If you need to migrate data from unencrypted fields to encrypted fields, three ways to solve it: 1. When there's no data in the db it should be possible to start from scratch by recreating the db 1. When there's no data in the table it should be possible to recreate the table 1. When there's data or if the project is shared it should be possible to do it in a non destructive way **Option 1: No data is in the db** 1. Drop the database 1. Squash the migrations 1. Recreate the db **Option 2: No data in the table** 1. Create a migration to drop the table 1. Create a new migration for the table with the encrypted field 1. Optionally squash the migration **Option 3: Migrating in a non-destructive way** The goal here is to be able to use to legacy field if something goes wrong. Part 1: 1. Create new field 1. When data is saved write both to legacy and new field 1. Create a data migration to cast data from legacy field to new field 1. check existing data from legacy and new field are the same if possible Part 2: 1. Rename the fields and drop legacy fields 1. Update the code to use only the new field ## Common Errors ### `psycopg2.errors.UndefinedFunction: function pgp_sym_encrypt(numeric, unknown) does not exist` This commonly means you do not have the `pgcrypto` extension installed in Postgres. Run the migration available in this library or install it manually in pgsql console. ## Security Limitations Taken direction from the PostgreSQL documentation: https://www.postgresql.org/docs/9.6/static/pgcrypto.html#AEN187024 All pgcrypto functions run inside the database server. That means that all the data and passwords move between pgcrypto and client applications in clear text. Thus you must: 1. Connect locally or use SSL connections. 1. Trust both system and database administrator. If you cannot, then better do crypto inside client application. The implementation does not resist side-channel attacks. For example, the time required for a pgcrypto decryption function to complete varies among ciphertexts of a given size. ================================================ FILE: pgcrypto/__init__.py ================================================ DIGEST_SQL = "digest(%s, 'sha512')" HMAC_SQL = "hmac(%s, '{}', 'sha512')" PGP_PUB_ENCRYPT_SQL_WITH_NULLIF = "pgp_pub_encrypt(nullif(%s, NULL)::text, dearmor('{}'))" PGP_SYM_ENCRYPT_SQL_WITH_NULLIF = "pgp_sym_encrypt(nullif(%s, NULL)::text, '{}')" PGP_PUB_ENCRYPT_SQL = "pgp_pub_encrypt(%s, dearmor('{}'))" PGP_SYM_ENCRYPT_SQL = "pgp_sym_encrypt(%s, '{}')" PGP_PUB_DECRYPT_SQL = "pgp_pub_decrypt(%s, dearmor('{}'))::%s" PGP_SYM_DECRYPT_SQL = "pgp_sym_decrypt(%s, '{}')::%s" ================================================ FILE: pgcrypto/fields.py ================================================ from django.db import models from pgcrypto import ( DIGEST_SQL, HMAC_SQL, PGP_PUB_ENCRYPT_SQL_WITH_NULLIF, PGP_SYM_ENCRYPT_SQL_WITH_NULLIF, ) from pgcrypto.lookups import ( HashLookup, ) from pgcrypto.mixins import ( DecimalPGPFieldMixin, get_setting, HashMixin, PGPPublicKeyFieldMixin, PGPSymmetricKeyFieldMixin, ) class TextDigestField(HashMixin, models.TextField): """Text digest field for postgres.""" encrypt_sql = DIGEST_SQL def get_encrypt_sql(self, connection): """Get encrypt sql.""" return self.encrypt_sql.format(get_setting(connection, 'PGCRYPTO_KEY')) TextDigestField.register_lookup(HashLookup) class TextHMACField(HashMixin, models.TextField): """Text HMAC field for postgres.""" encrypt_sql = HMAC_SQL TextHMACField.register_lookup(HashLookup) class EmailPGPPublicKeyField(PGPPublicKeyFieldMixin, models.EmailField): """Email PGP public key encrypted field.""" class IntegerPGPPublicKeyField(PGPPublicKeyFieldMixin, models.IntegerField): """Integer PGP public key encrypted field.""" encrypt_sql = PGP_PUB_ENCRYPT_SQL_WITH_NULLIF cast_type = 'INT4' class BigIntegerPGPPublicKeyField(PGPPublicKeyFieldMixin, models.IntegerField): """BigInteger PGP public key encrypted field.""" encrypt_sql = PGP_PUB_ENCRYPT_SQL_WITH_NULLIF cast_type = 'BIGINT' class TextPGPPublicKeyField(PGPPublicKeyFieldMixin, models.TextField): """Text PGP public key encrypted field.""" class CharPGPPublicKeyField(PGPPublicKeyFieldMixin, models.CharField): """Char PGP public key encrypted field.""" class DatePGPPublicKeyField(PGPPublicKeyFieldMixin, models.DateField): """Date PGP public key encrypted field for postgres.""" encrypt_sql = PGP_PUB_ENCRYPT_SQL_WITH_NULLIF cast_type = 'DATE' class DateTimePGPPublicKeyField(PGPPublicKeyFieldMixin, models.DateTimeField): """DateTime PGP public key encrypted field for postgres.""" encrypt_sql = PGP_PUB_ENCRYPT_SQL_WITH_NULLIF cast_type = 'TIMESTAMPTZ' class BooleanPGPPublicKeyField(PGPPublicKeyFieldMixin, models.BooleanField): """Boolean PGP public key encrypted field.""" encrypt_sql = PGP_PUB_ENCRYPT_SQL_WITH_NULLIF cast_type = 'BOOL' class EmailPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.EmailField): """Email PGP symmetric key encrypted field.""" class IntegerPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.IntegerField): """Integer PGP symmetric key encrypted field.""" encrypt_sql = PGP_SYM_ENCRYPT_SQL_WITH_NULLIF cast_type = 'INT4' class BigIntegerPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.IntegerField): """BigInteger PGP symmetric key encrypted field.""" encrypt_sql = PGP_SYM_ENCRYPT_SQL_WITH_NULLIF cast_type = 'BIGINT' class TextPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.TextField): """Text PGP symmetric key encrypted field for postgres.""" class CharPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.CharField): """Char PGP symmetric key encrypted field for postgres.""" class DatePGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.DateField): """Date PGP symmetric key encrypted field for postgres.""" encrypt_sql = PGP_SYM_ENCRYPT_SQL_WITH_NULLIF cast_type = 'DATE' class DateTimePGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.DateTimeField): """DateTime PGP symmetric key encrypted field for postgres.""" encrypt_sql = PGP_SYM_ENCRYPT_SQL_WITH_NULLIF cast_type = 'TIMESTAMPTZ' class BooleanPGPSymmetricKeyField(PGPPublicKeyFieldMixin, models.BooleanField): """Boolean PGP public key encrypted field.""" encrypt_sql = PGP_PUB_ENCRYPT_SQL_WITH_NULLIF cast_type = 'BOOL' class DecimalPGPPublicKeyField(DecimalPGPFieldMixin, PGPPublicKeyFieldMixin, models.DecimalField): """Decimal PGP public key encrypted field for postgres.""" class DecimalPGPSymmetricKeyField(DecimalPGPFieldMixin, PGPSymmetricKeyFieldMixin, models.DecimalField): """Decimal PGP symmetric key encrypted field for postgres.""" class FloatPGPPublicKeyField(PGPPublicKeyFieldMixin, models.FloatField): """Float PGP public key encrypted field for postgres.""" encrypt_sql = PGP_PUB_ENCRYPT_SQL_WITH_NULLIF cast_type = 'DOUBLE PRECISION' class FloatPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.FloatField): """Float PGP symmetric key encrypted field for postgres.""" encrypt_sql = PGP_SYM_ENCRYPT_SQL_WITH_NULLIF cast_type = 'DOUBLE PRECISION' class TimePGPPublicKeyField(PGPPublicKeyFieldMixin, models.TimeField): """Time PGP public key encrypted field for postgres.""" encrypt_sql = PGP_PUB_ENCRYPT_SQL_WITH_NULLIF cast_type = 'TIME' class TimePGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.TimeField): """Float PGP symmetric key encrypted field for postgres.""" encrypt_sql = PGP_SYM_ENCRYPT_SQL_WITH_NULLIF cast_type = 'TIME' ================================================ FILE: pgcrypto/lookups.py ================================================ from django.db.models.lookups import Lookup class HashLookup(Lookup): """Lookup to filter hashed values. `HashLookup` is hashing the value on the right hand side with the function specified in `encrypt_sql`. """ lookup_name = 'hash_of' def as_sql(self, qn, connection): """Responsible for creating the lookup with the digest SQL. Modify the right hand side expression to compare the value passed to a hash. """ lhs, lhs_params = self.process_lhs(qn, connection) rhs, rhs_params = self.process_rhs(qn, connection) params = lhs_params + rhs_params rhs = self.lhs.field.encrypt_sql % rhs return ('{}::bytea = {}'.format(lhs, rhs)), params ================================================ FILE: pgcrypto/migrations/0001_add_pgcrypto_extension.py ================================================ from django.contrib.postgres.operations import CreateExtension from django.db import migrations class Migration(migrations.Migration): dependencies = [] operations = [ CreateExtension('pgcrypto'), ] ================================================ FILE: pgcrypto/migrations/__init__.py ================================================ ================================================ FILE: pgcrypto/mixins.py ================================================ from django.conf import settings from django.db.models.expressions import Col from django.utils.functional import cached_property from pgcrypto import ( PGP_PUB_DECRYPT_SQL, PGP_PUB_ENCRYPT_SQL, PGP_SYM_DECRYPT_SQL, PGP_SYM_ENCRYPT_SQL, ) def get_setting(connection, key): """Get key from connection or default to settings.""" if key in connection.settings_dict: return connection.settings_dict[key] else: return getattr(settings, key) class DecryptedCol(Col): """Provide DecryptedCol support without using `extra` sql.""" def __init__(self, alias, target, output_field=None): """Init the decryption.""" self.target = target super(DecryptedCol, self).__init__(alias, target, output_field) def as_sql(self, compiler, connection): """Build SQL with decryption and casting.""" sql, params = super(DecryptedCol, self).as_sql(compiler, connection) sql = self.target.get_decrypt_sql(connection) % (sql, self.target.get_cast_sql()) return sql, params class HashMixin: """Keyed hash mixin. `HashMixin` uses 'pgcrypto' to encrypt data in a postgres database. """ encrypt_sql = None # Set in implementation class def __init__(self, original=None, *args, **kwargs): """Tells the init the original attr.""" self.original = original super(HashMixin, self).__init__(*args, **kwargs) def pre_save(self, model_instance, add): """Save the original_value.""" if self.original: original_value = getattr(model_instance, self.original) setattr(model_instance, self.attname, original_value) return super(HashMixin, self).pre_save(model_instance, add) def get_placeholder(self, value=None, compiler=None, connection=None): """ Tell postgres to encrypt this field with a hashing function. The `value` string is checked to determine if we need to hash or keep the current value. `compiler` and `connection` is ignored here as we don't need custom operators. """ if value is None or value.startswith('\\x'): return '%s' return self.get_encrypt_sql(connection) def get_encrypt_sql(self, connection): """Get encrypt sql. This may be overidden by some implementations.""" return self.encrypt_sql class PGPMixin: """PGP encryption for field's value. `PGPMixin` uses 'pgcrypto' to encrypt data in a postgres database. """ encrypt_sql = None # Set in implementation class decrypt_sql = None # Set in implementation class cast_type = None def __init__(self, *args, **kwargs): """`max_length` should be set to None as encrypted text size is variable.""" super().__init__(*args, **kwargs) def db_type(self, connection=None): """Value stored in the database is hexadecimal.""" return 'bytea' def get_placeholder(self, value, compiler, connection): """Tell postgres to encrypt this field using PGP.""" raise NotImplementedError('The `get_placeholder` needs to be implemented.') def get_cast_sql(self): """Get cast sql. This may be overidden by some implementations.""" return self.cast_type def get_decrypt_sql(self, connection): """Get decrypt sql.""" raise NotImplementedError('The `get_decrypt_sql` needs to be implemented.') def get_col(self, alias, output_field=None): """Get the decryption for col.""" if output_field is None: output_field = self if alias != self.model._meta.db_table or output_field != self: return DecryptedCol( alias, self, output_field ) else: return self.cached_col @cached_property def cached_col(self): """Get cached version of decryption for col.""" return DecryptedCol( self.model._meta.db_table, self ) class PGPPublicKeyFieldMixin(PGPMixin): """PGP public key encrypted field mixin for postgres.""" encrypt_sql = PGP_PUB_ENCRYPT_SQL decrypt_sql = PGP_PUB_DECRYPT_SQL cast_type = 'TEXT' def get_placeholder(self, value=None, compiler=None, connection=None): """Tell postgres to encrypt this field using PGP.""" return self.encrypt_sql.format(get_setting(connection, 'PUBLIC_PGP_KEY')) def get_decrypt_sql(self, connection): """Get decrypt sql.""" return self.decrypt_sql.format(get_setting(connection, 'PRIVATE_PGP_KEY')) class PGPSymmetricKeyFieldMixin(PGPMixin): """PGP symmetric key encrypted field mixin for postgres.""" encrypt_sql = PGP_SYM_ENCRYPT_SQL decrypt_sql = PGP_SYM_DECRYPT_SQL cast_type = 'TEXT' def get_placeholder(self, value, compiler, connection): """Tell postgres to encrypt this field using PGP.""" return self.encrypt_sql.format(get_setting(connection, 'PGCRYPTO_KEY')) def get_decrypt_sql(self, connection): """Get decrypt sql.""" return self.decrypt_sql.format(get_setting(connection, 'PGCRYPTO_KEY')) class DecimalPGPFieldMixin: """Decimal PGP encrypted field mixin for postgres.""" cast_type = 'NUMERIC(%(max_digits)s, %(decimal_places)s)' def get_cast_sql(self): """Get cast sql.""" return self.cast_type % { 'max_digits': self.max_digits, 'decimal_places': self.decimal_places } ================================================ FILE: pgcrypto/models.py ================================================ ================================================ FILE: requirements.txt ================================================ -e . django>=1.11,<3.2 ================================================ FILE: requirements_dev.txt ================================================ colour-runner==0.1.1 coveralls==3.1.0 coverage==5.5 dj-database-url==0.5.0 factory-boy==3.2.0 flake8-docstrings==1.6.0 flake8-import-order==0.18.1 flake8==3.9.2 incuna-test-utils==8.0.0 pip==22.3 psycopg2-binary==2.8.6 pyflakes==2.3.1 pycodestyle==2.7.0 setuptools==65.5.0 twine==3.4.1 wheel==0.36.2 ================================================ FILE: setup.cfg ================================================ [flake8] max-line-length = 90 max-complexity = 10 exclude = *migrations*, venv ignore = D100,D101,D104,D203,D204 statistics=true application-import-names = pgcrypto import-order-style = smarkets [coverage:run] source = pgcrypto omit = *migrations*, *tests* [coverage:report] show_missing = true exclude_lines = raise NotImplementedError ================================================ FILE: setup.py ================================================ import sys from setuptools import find_packages, setup CURRENT_PYTHON = sys.version_info[:2] REQUIRED_PYTHON = (3, 6) # This check and everything above must remain compatible with Python 2.7. if CURRENT_PYTHON < REQUIRED_PYTHON: sys.stderr.write(""" ========================== Unsupported Python version ========================== This version of django-pgcrypto-fields requires Python {}.{}, but you're trying to install it on Python {}.{}. This may be because you are using a version of pip that doesn't understand the python_requires classifier. Make sure you have pip >= 9.0 and setuptools >= 24.2, then try again: $ python -m pip install --upgrade pip setuptools $ python -m pip install django-pgcrypto-fields """.format(*(REQUIRED_PYTHON + CURRENT_PYTHON))) sys.exit(1) with open('README.md') as readme_file: readme = readme_file.read() with open('CHANGELOG.md') as changelog_file: changelog = changelog_file.read() version = '2.6.0' setup( name='django-pgcrypto-fields', packages=find_packages(exclude=['tests']), include_package_data=True, version=version, python_requires='>={}.{}'.format(*REQUIRED_PYTHON), license='BSD', description='Encrypted fields for Django dealing with pgcrypto postgres extension.', long_description=readme + '\n\n' + changelog, long_description_content_type='text/markdown', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Framework :: Django', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Database', 'Topic :: Security :: Cryptography', ], author='Incuna Ltd', author_email='admin@incuna.com', url='https://github.com/incuna/django-pgcrypto-fields', test_suite='tests', ) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/dbrouters.py ================================================ class TestRouter(object): def db_for_read(self, model, **hints): """Read from diff_keys.""" if model._meta.app_label == 'diff_keys': return 'diff_keys' return 'default' def db_for_write(self, model, **hints): """Write to diff_keys.""" if model._meta.app_label == 'diff_keys': return 'diff_keys' return 'default' ================================================ FILE: tests/default/__init__.py ================================================ ================================================ FILE: tests/diff_keys/__init__.py ================================================ ================================================ FILE: tests/diff_keys/models.py ================================================ from django.db import models from pgcrypto import fields class EncryptedDiff(models.Model): CHOICES = ( ('a', 'a'), (1, '1'), ) pub_field = fields.CharPGPPublicKeyField(blank=True, null=True, choices=CHOICES, max_length=1) sym_field = fields.CharPGPSymmetricKeyField(blank=True, null=True, choices=CHOICES, max_length=1) digest_field = fields.TextDigestField(blank=True, null=True) hmac_field = fields.TextHMACField(blank=True, null=True) class Meta: """Sets up the meta for the test model.""" app_label = 'diff_keys' ================================================ FILE: tests/factories.py ================================================ from datetime import date, datetime from decimal import Decimal import factory from .models import EncryptedFKModel, EncryptedModel class EncryptedFKModelFactory(factory.django.DjangoModelFactory): """Factory to generate foreign key data.""" fk_pgp_sym_field = factory.Sequence('Text with symmetric key {}'.format) class Meta: """Sets up meta for test factory.""" model = EncryptedFKModel class EncryptedModelFactory(factory.django.DjangoModelFactory): """Factory to generate hashed and encrypted data.""" digest_field = factory.Sequence('Text digest {}'.format) hmac_field = factory.Sequence('Text hmac {}'.format) email_pgp_pub_field = factory.Sequence('email{}@public.key'.format) integer_pgp_pub_field = 42 biginteger_pgp_pub_field = 9223372036854775807 pgp_pub_field = factory.Sequence('Text with public key {}'.format) char_pub_field = factory.Sequence('Text {}'.format) date_pgp_pub_field = date.today() datetime_pgp_pub_field = datetime.now() decimal_pgp_pub_field = Decimal('123456.78') boolean_pgp_pub_field = True email_pgp_sym_field = factory.Sequence('email{}@symmetric.key'.format) integer_pgp_sym_field = 43 biginteger_pgp_sym_field = 9223372036854775807 pgp_sym_field = factory.Sequence('Text with symmetric key {}'.format) char_sym_field = factory.Sequence('Text {}'.format) date_pgp_sym_field = date.today() datetime_pgp_sym_field = datetime.now() boolean_pgp_sym_field = False fk_model = factory.SubFactory(EncryptedFKModelFactory) class Meta: """Sets up meta for test factory.""" model = EncryptedModel ================================================ FILE: tests/forms.py ================================================ from django import forms from .models import EncryptedModel class EncryptedForm(forms.ModelForm): """Test for EncryptedModel.""" class Meta: """Meta for form.""" model = EncryptedModel fields = '__all__' widgets = { 'date_pgp_sym_field': forms.DateInput(format='%m/%d/%Y'), 'datetime_pgp_sym_field': forms.DateTimeInput(format='%m/%d/%Y %I:%M'), 'date_pgp_pub_field': forms.DateInput(format='%m/%d/%Y'), 'datetime_pgp_pub_field': forms.DateTimeInput(format='%m/%d/%Y %I:%M'), } ================================================ FILE: tests/keys/README.md ================================================ Dragon ahead, do not use these keys. They have been generated for testing purpose. You should never expose your private/secret key. ================================================ FILE: tests/keys/private.key ================================================ -----BEGIN PGP PRIVATE KEY BLOCK----- Version: GnuPG v1 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 =kJd7 -----END PGP PRIVATE KEY BLOCK----- ================================================ FILE: tests/keys/private_diff.key ================================================ -----BEGIN PGP PRIVATE KEY BLOCK----- Version: GnuPG v1 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 zhkqb4tHw1kFgzXsT9FuzF+r =JjTF -----END PGP PRIVATE KEY BLOCK----- ================================================ FILE: tests/keys/public.key ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1 mQENBFRJDkwBCADVJDHy7al1P8urXWDkTSMfb6vGivD6vW0N2dDFixxKHDlOej52 OB2F/B/WLFpdXFiKxLSTIWEp/d0AEtJQrNMuMCzUKT08h3YU5ypiBdGIY7fqWwi9 G18ihm29/ygPni1XzG02DCto/pYD++RieIgGPOm8xlLrHwemehxhiOQ1K9eds8wd d0awXtiKdClaxGi24soyBxBC/tLde2Sgyh5y24RyXsmvZxORwWBM5tRZwOxwsQym Akt2M7LIGyQVgbLmTe4k5u3Uqv/t5tVK8OdDDbbXVDwJbj4n1Jol68cutHTAahpY mkOAuqLp7ZWqx/DsYYoJhv/G7dhcpS+CfNs3ABEBAAG0HlRlc3QgS2V5IDxleGFt cGxlQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCVEkOTAIbAwYLCQgHAwIGFQgCCQoL BBYCAwECHgECF4AACgkQY3+K26BCfZUZPwgAzM03DFGATxldBcia63gK+vsEoDS+ xCtHaDpNvestHDlCd3bHU13LcLAXUD9E/s4LpHF+lMACCyscdeTriA/x0IJBIuq9 4KXMHJPpsh+eyGn09zhSfcfoMnQqgD682tvtBAD+oPb7Z/q+FNbKH26Pq6Daw1Ap gDEy9AMabBoGx8db878OFk6eNEZliF38QqRplqr/hsjRq4nwkAAT3qj1zufTTN90 bHhXt1BP7z5bDv0z1fp6duFzPbfyUEjkhbFKhOgJ6p01IOLnnXEn6TGM55sKqW1W dFX276anyb1gJm79E3gymnuwanC31QNSmlXpSNbiG8rbUfiJ40HfUf/lKrkBDQRU SQ5MAQgAnztdietX1+rTHqyjK9yDYP+rp5NBL2b98SzTjQpFv68cjevjYy/R7VqI rRx1qyrit470TYk9iOH2KBYscLLYylNCfpWfv2gIziZfX5Xxb+BxZPXkrq34ux5M 1BHgGvZg8XvtJLSbX5SnkibUHSotCD578MgfWzfTfH4aeVwrZVCKb5BRxZHx0ZA9 q9tNgYWoadhcKNWmT8yn28JW/ME91v1K/nSDJKd8qG9qOEy0XtGcYOZh6qZZcXI3 BRpyqSUN4G2TvzccYeEbw6hHH2VjKCetkt9412+2Qg+IR8nTehwPuY3hOZqYghN/ rT6nuPJSzb/UJ+G6vnb2r0I4To3WUQARAQABiQEfBBgBAgAJBQJUSQ5MAhsMAAoJ EGN/itugQn2VlBcH/2dRT5yxfS9nTTimfk/wnyXnB+XgqbYOr7H1LFueMulCTSrQ sIebVUyIKY2Txzm8UxswMBxRzoIMy8g6NxjwuUzm3/w2evdHdO1mwqNMsDNBskPD CzJE4TLftOeVq7Jh268yLFiAxaAbULyBzr7yLVAHrGBDhjediCoy9KHijIHdM04v nhgQkV6UpLez7Am5B0Pqd+0kFcoe3IW5cYvphhWVJ4O+w95jujrsvVB/+hg4VPgi KMgIdXIAmJQuflAU97ilYDIXXmdQV2yUOsnaMW8JBsu2hsxvk3n+M9BNS6GBk40w HGCOLDrMD1U4Zjwsu58qQGp4nQ9rR54y5El2iJU= =c2SM -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: tests/keys/public_diff.key ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1 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 MTLctweH+r/pbR1m0mXfLKdjs2ApAG4zMoJdnK0= =dyCW -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: tests/models.py ================================================ from django.db import models from pgcrypto import fields class EncryptedFKModel(models.Model): """Dummy model used to test FK decryption.""" fk_pgp_sym_field = fields.TextPGPSymmetricKeyField(blank=True, null=True) class Meta: """Sets up the meta for the test model.""" app_label = 'tests' class EncryptedModelManager(models.Manager): def get_by_natural_key(self, email_pgp_pub_field): """Get by natual key of email pub field.""" return self.get(email_pgp_pub_field=email_pgp_pub_field) class EncryptedModel(models.Model): """Dummy model used for tests to check the fields.""" digest_field = fields.TextDigestField(blank=True, null=True) digest_with_original_field = fields.TextDigestField(blank=True, null=True, original='pgp_sym_field') hmac_field = fields.TextHMACField(blank=True, null=True) hmac_with_original_field = fields.TextHMACField(blank=True, null=True, original='pgp_sym_field') email_pgp_pub_field = fields.EmailPGPPublicKeyField(blank=True, null=True, unique=True) integer_pgp_pub_field = fields.IntegerPGPPublicKeyField(blank=True, null=True) biginteger_pgp_pub_field = fields.BigIntegerPGPPublicKeyField(blank=True, null=True) pgp_pub_field = fields.TextPGPPublicKeyField(blank=True, null=True) char_pub_field = fields.CharPGPPublicKeyField(blank=True, null=True, max_length=15) date_pgp_pub_field = fields.DatePGPPublicKeyField(blank=True, null=True) datetime_pgp_pub_field = fields.DateTimePGPPublicKeyField(blank=True, null=True) time_pgp_pub_field = fields.TimePGPPublicKeyField(blank=True, null=True) decimal_pgp_pub_field = fields.DecimalPGPPublicKeyField( max_digits=8, decimal_places=2, null=True, blank=True ) float_pgp_pub_field = fields.FloatPGPPublicKeyField(blank=True, null=True) boolean_pgp_pub_field = fields.BooleanPGPPublicKeyField(blank=True, null=True) email_pgp_sym_field = fields.EmailPGPSymmetricKeyField(blank=True, null=True) integer_pgp_sym_field = fields.IntegerPGPSymmetricKeyField(blank=True, null=True) biginteger_pgp_sym_field = fields.BigIntegerPGPSymmetricKeyField( blank=True, null=True ) pgp_sym_field = fields.TextPGPSymmetricKeyField(blank=True, null=True) char_sym_field = fields.CharPGPPublicKeyField(blank=True, null=True, max_length=15) date_pgp_sym_field = fields.DatePGPSymmetricKeyField(blank=True, null=True) datetime_pgp_sym_field = fields.DateTimePGPSymmetricKeyField(blank=True, null=True) time_pgp_sym_field = fields.TimePGPSymmetricKeyField(blank=True, null=True) decimal_pgp_sym_field = fields.DecimalPGPSymmetricKeyField( max_digits=8, decimal_places=2, null=True, blank=True ) float_pgp_sym_field = fields.FloatPGPSymmetricKeyField(blank=True, null=True) boolean_pgp_sym_field = fields.BooleanPGPSymmetricKeyField(blank=True, null=True) fk_model = models.ForeignKey( EncryptedFKModel, blank=True, null=True, on_delete=models.CASCADE ) objects = EncryptedModelManager() class Meta: """Sets up the meta for the test model.""" app_label = 'tests' class EncryptedDateTime(models.Model): value = fields.DateTimePGPSymmetricKeyField() class RelatedDateTime(models.Model): related = models.ForeignKey( EncryptedDateTime, on_delete=models.CASCADE, related_name='related') related_again = models.ForeignKey( EncryptedDateTime, null=True, on_delete=models.CASCADE, related_name='related_again' ) ================================================ FILE: tests/run.py ================================================ #! /usr/bin/env python """From http://stackoverflow.com/a/12260597/400691.""" import os import sys import dj_database_url import django from colour_runner.django_runner import ColourRunnerMixin from django.conf import settings from django.test.runner import DiscoverRunner BASEDIR = os.path.dirname(os.path.dirname(__file__)) PUBLIC_PGP_KEY_PATH = os.path.abspath( os.path.join(BASEDIR, 'tests/keys/public.key') ) PRIVATE_PGP_KEY_PATH = os.path.abspath( os.path.join(BASEDIR, 'tests/keys/private.key') ) DIFF_PUBLIC_PGP_KEY_PATH = os.path.abspath( os.path.join(BASEDIR, 'tests/keys/public_diff.key') ) DIFF_PRIVATE_PGP_KEY_PATH = os.path.abspath( os.path.join(BASEDIR, 'tests/keys/private_diff.key') ) diff_keys = dj_database_url.config( default='postgres://localhost/pgcrypto_fields_diff' ) # Cannot chain onto the config() call due to error diff_keys.update({ 'PUBLIC_PGP_KEY': open(DIFF_PUBLIC_PGP_KEY_PATH, 'r').read(), 'PRIVATE_PGP_KEY': open(DIFF_PRIVATE_PGP_KEY_PATH, 'r').read(), 'PGCRYPTO_KEY': 'djangorocks', }) settings.configure( DATABASES={ 'default': dj_database_url.config( default='postgres://localhost/pgcrypto_fields' ), 'diff_keys': diff_keys, }, INSTALLED_APPS=( 'pgcrypto', "tests.diff_keys", 'tests', ), DATABASE_ROUTERS=('dbrouters.TestRouter',), MIDDLEWARE_CLASSES=(), PUBLIC_PGP_KEY=open(PUBLIC_PGP_KEY_PATH, 'r').read(), PRIVATE_PGP_KEY=open(PRIVATE_PGP_KEY_PATH, 'r').read(), PGCRYPTO_KEY='ultrasecret', DEBUG=True, ) django.setup() class TestRunner(ColourRunnerMixin, DiscoverRunner): """Enable colorised output.""" test_runner = TestRunner(verbosity=1) failures = test_runner.run_tests(['tests']) if failures: sys.exit(1) ================================================ FILE: tests/test_fields.py ================================================ from datetime import date, datetime from decimal import Decimal from unittest.mock import MagicMock from django import VERSION as DJANGO_VERSION from django.conf import settings from django.db import connections, models, reset_queries from django.test import TestCase from incuna_test_utils.utils import field_names from pgcrypto import fields from .diff_keys.models import EncryptedDiff from .factories import EncryptedFKModelFactory, EncryptedModelFactory from .forms import EncryptedForm from .models import EncryptedDateTime, EncryptedFKModel, \ EncryptedModel, RelatedDateTime KEYED_FIELDS = (fields.TextDigestField, fields.TextHMACField) EMAIL_PGP_FIELDS = (fields.EmailPGPPublicKeyField, fields.EmailPGPSymmetricKeyField) PGP_FIELDS = EMAIL_PGP_FIELDS + ( fields.DatePGPSymmetricKeyField, fields.DateTimePGPSymmetricKeyField, fields.IntegerPGPPublicKeyField, fields.IntegerPGPSymmetricKeyField, fields.TextPGPPublicKeyField, fields.TextPGPSymmetricKeyField, fields.BooleanPGPPublicKeyField, fields.BooleanPGPSymmetricKeyField, ) class TestTextFieldHash(TestCase): """Test hash fields behave properly.""" def test_get_placeholder(self): """Assert `get_placeholder` hash value only once.""" for field in KEYED_FIELDS: with self.subTest(field=field): placeholder = field().get_placeholder('\\x') self.assertEqual(placeholder, '%s') class TestPGPMixin(TestCase): databases = '__all__' """Test `PGPMixin` behave properly.""" def test_check(self): """Assert `max_length` check does not return any error.""" for field in PGP_FIELDS: with self.subTest(field=field): field.model = MagicMock() self.assertEqual(field(name='field').check(), []) def test_db_type(self): """Check db_type is `bytea`.""" for field in PGP_FIELDS: with self.subTest(field=field): self.assertEqual(field().db_type(), 'bytea') class TestEmailPGPMixin(TestCase): """Test emails fields behave properly.""" def test_max_length_validator(self): """Check `MaxLengthValidator` is not set.""" for field in EMAIL_PGP_FIELDS: with self.subTest(field=field): field_validated = field().run_validators(value='value@value.com') self.assertEqual(field_validated, None) class TestEncryptedTextFieldModel(TestCase): databases = '__all__' """Test `EncryptedTextField` can be integrated in a `Django` model.""" model = EncryptedModel # You have to do it here or queries is empty settings.DEBUG = True def test_fields(self): """Assert fields are representing our model.""" fields = field_names(self.model) expected = ( 'id', 'digest_field', 'digest_with_original_field', 'hmac_field', 'hmac_with_original_field', 'email_pgp_pub_field', 'integer_pgp_pub_field', 'biginteger_pgp_pub_field', 'pgp_pub_field', 'char_pub_field', 'decimal_pgp_pub_field', 'email_pgp_sym_field', 'integer_pgp_sym_field', 'biginteger_pgp_sym_field', 'pgp_sym_field', 'char_sym_field', 'date_pgp_sym_field', 'datetime_pgp_sym_field', 'time_pgp_sym_field', 'date_pgp_pub_field', 'datetime_pgp_pub_field', 'time_pgp_pub_field', 'decimal_pgp_sym_field', 'float_pgp_pub_field', 'float_pgp_sym_field', 'boolean_pgp_pub_field', 'boolean_pgp_sym_field', 'fk_model', ) self.assertCountEqual(fields, expected) def test_value_returned_is_not_bytea(self): """Assert value returned is not a memoryview instance.""" EncryptedModelFactory.create() instance = self.model.objects.get() self.assertIsInstance(instance.digest_field, str) self.assertIsInstance(instance.hmac_field, str) self.assertIsInstance(instance.email_pgp_pub_field, str) self.assertIsInstance(instance.integer_pgp_pub_field, int) self.assertIsInstance(instance.biginteger_pgp_pub_field, int) self.assertIsInstance(instance.pgp_pub_field, str) self.assertIsInstance(instance.date_pgp_pub_field, date) self.assertIsInstance(instance.datetime_pgp_pub_field, datetime) self.assertIsInstance(instance.email_pgp_sym_field, str) self.assertIsInstance(instance.integer_pgp_sym_field, int) self.assertIsInstance(instance.biginteger_pgp_sym_field, int) self.assertIsInstance(instance.pgp_sym_field, str) self.assertIsInstance(instance.date_pgp_sym_field, date) self.assertIsInstance(instance.datetime_pgp_sym_field, datetime) def test_value_query(self): """Assert querying the field's value is making zero queries.""" expected = 'bonjour' temp = None EncryptedModelFactory.create(pgp_pub_field=expected) instance = self.model.objects.get() with self.assertNumQueries(0): temp = instance.pgp_pub_field self.assertEqual(expected, temp) def test_value_pgp_pub(self): """Assert we can get back the decrypted value.""" expected = 'bonjour' EncryptedModelFactory.create(pgp_pub_field=expected) instance = self.model.objects.get() value = instance.pgp_pub_field self.assertEqual(value, expected) def test_value_pgp_pub_multiple(self): """Assert we get back the correct value when the table contains data.""" expected = 'bonjour' EncryptedModelFactory.create(pgp_pub_field='au revoir') created = EncryptedModelFactory.create(pgp_pub_field=expected) instance = self.model.objects.get(pk=created.pk) value = instance.pgp_pub_field self.assertEqual(value, expected) def test_value_pgp_sym(self): """Assert we can get back the decrypted value.""" expected = 'bonjour' EncryptedModelFactory.create(pgp_sym_field=expected) instance = self.model.objects.get() value = instance.pgp_sym_field self.assertEqual(value, expected) def test_instance_not_saved(self): """Assert not saved instance return the value to be encrypted.""" expected = 'bonjour' instance = EncryptedModelFactory.build(pgp_pub_field=expected) self.assertEqual(instance.pgp_pub_field, expected) self.assertEqual(instance.pgp_pub_field, expected) def test_decrypt_filter(self): """Assert we can get filter the decrypted value.""" expected = 'bonjour' EncryptedModelFactory.create( pgp_pub_field=expected, ) queryset = self.model.objects.filter( pgp_pub_field=expected ) instance = queryset.first() self.assertEqual(instance.pgp_pub_field, expected) queryset = self.model.objects.filter( pgp_pub_field__contains='jour' ) instance = queryset.first() self.assertEqual(instance.pgp_pub_field, expected) queryset = self.model.objects.filter( pgp_pub_field__startswith='bon' ) instance = queryset.first() self.assertEqual(instance.pgp_pub_field, expected) def test_digest_lookup(self): """Assert we can filter a digest value.""" value = 'bonjour' expected = EncryptedModelFactory.create(digest_field=value) EncryptedModelFactory.create() queryset = EncryptedModel.objects.filter(digest_field__hash_of=value) self.assertCountEqual(queryset, [expected]) def test_digest_with_original_lookup(self): """Assert we can filter a digest value.""" value = 'bonjour' expected = EncryptedModelFactory.create(pgp_sym_field=value) EncryptedModelFactory.create() queryset = EncryptedModel.objects.filter( digest_with_original_field__hash_of=value ) self.assertCountEqual(queryset, [expected]) def test_hmac_lookup(self): """Assert we can filter a digest value.""" value = 'bonjour' expected = EncryptedModelFactory.create(hmac_field=value) EncryptedModelFactory.create() queryset = EncryptedModel.objects.filter(hmac_field__hash_of=value) self.assertCountEqual(queryset, [expected]) def test_hmac_with_original_lookup(self): """Assert we can filter a digest value.""" value = 'bonjour' expected = EncryptedModelFactory.create(pgp_sym_field=value) EncryptedModelFactory.create() queryset = EncryptedModel.objects.filter(hmac_with_original_field__hash_of=value) self.assertCountEqual(queryset, [expected]) def test_default_lookup(self): """Assert default lookup can be called.""" queryset = EncryptedModel.objects.filter(hmac_field__isnull=True) self.assertFalse(queryset) def test_update_attribute_digest_field(self): """Assert digest field can be updated through its attribute on the model.""" expected = 'bonjour' instance = EncryptedModelFactory.create() instance.digest_field = expected instance.save() updated_instance = self.model.objects.filter(digest_field__hash_of=expected) self.assertEqual(updated_instance.first(), instance) def test_update_attribute_hmac_field(self): """Assert hmac field can be updated through its attribute on the model.""" expected = 'bonjour' instance = EncryptedModelFactory.create() instance.hmac_field = expected instance.save() updated_instance = self.model.objects.filter(hmac_field__hash_of=expected) self.assertEqual(updated_instance.first(), instance) def test_update_attribute_pgp_pub_field(self): """Assert pgp field can be updated through its attribute on the model.""" expected = 'bonjour' instance = EncryptedModelFactory.create() instance.pgp_pub_field = expected instance.save() updated_instance = self.model.objects.get() self.assertEqual(updated_instance.pgp_pub_field, expected) def test_update_attribute_pgp_sym_field(self): """Assert pgp field can be updated through its attribute on the model.""" expected = 'bonjour' instance = EncryptedModelFactory.create() instance.pgp_sym_field = expected instance.save() updated_instance = self.model.objects.get() self.assertEqual(updated_instance.pgp_sym_field, expected) def test_update_one_attribute(self): """Assert value are not overriden when updating one attribute.""" expected = 'initial value' new_value = 'new_value' instance = EncryptedModelFactory.create( pgp_pub_field=expected, pgp_sym_field=expected, digest_field=expected, hmac_field=expected, ) instance.pgp_sym_field = new_value instance.save() updated_instance = self.model.objects.get() self.assertEqual(updated_instance.pgp_pub_field, expected) self.assertEqual(updated_instance.pgp_sym_field, new_value) updated_instance = self.model.objects.filter( digest_field__hash_of=expected, hmac_field__hash_of=expected, ) self.assertEqual(updated_instance.first(), instance) def test_pgp_public_key_negative_number(self): """ Assert negative value is saved with Public Key integer fields. * `IntegerPGPPublicKeyField` * `BigIntegerPGPSymmetricKeyField` """ expected = -2147483648 instance = EncryptedModelFactory.create(integer_pgp_pub_field=expected) self.assertEqual(instance.integer_pgp_pub_field, expected) expected = -9223372036854775808 instance = EncryptedModelFactory.create(biginteger_pgp_pub_field=expected) self.assertEqual(instance.biginteger_pgp_pub_field, expected) def test_pgp_symmetric_key_negative_number(self): """ Assert negative value is saved with Symmetric Key fields. * `IntegerPGPSymmetricKeyField` * `BigIntegerPGPSymmetricKeyField` """ expected = -2147483648 instance = EncryptedModelFactory.create(integer_pgp_sym_field=expected) self.assertEqual(instance.integer_pgp_sym_field, expected) expected = -9223372036854775808 instance = EncryptedModelFactory.create(biginteger_pgp_sym_field=expected) self.assertEqual(instance.biginteger_pgp_sym_field, expected) def test_pgp_symmetric_key_date(self): """Assert date is save with an `DatePGPSymmetricKeyField` field.""" expected = date.today() instance = EncryptedModelFactory.create(date_pgp_sym_field=expected) instance.refresh_from_db() # Ensure the PGSQL casting works right self.assertEqual(instance.date_pgp_sym_field, expected) instance = EncryptedModel.objects.get(pk=instance.id) self.assertEqual(instance.date_pgp_sym_field, expected) def test_pgp_pub_key_date(self): """Assert date is save with an `DatePGPPublicKeyField` field.""" expected = date.today() instance = EncryptedModelFactory.create(date_pgp_pub_field=expected) instance.refresh_from_db() # Ensure the PGSQL casting works right self.assertEqual(instance.date_pgp_pub_field, expected) instance = EncryptedModel.objects.get(pk=instance.id) self.assertEqual(instance.date_pgp_pub_field, expected) def test_pgp_symmetric_key_date_form(self): """Assert form field and widget for `DateTimePGPSymmetricKeyField` field.""" expected = date.today() instance = EncryptedModelFactory.create(date_pgp_sym_field=expected) instance.refresh_from_db() # Ensure the PGSQL casting works right payload = { 'date_pgp_sym_field': '08/01/2016' } form = EncryptedForm(payload, instance=instance) self.assertTrue(form.is_valid()) cleaned_data = form.cleaned_data self.assertTrue( cleaned_data['date_pgp_sym_field'], date(2016, 8, 1) ) def test_pgp_symmetric_key_datetime_form(self): """Assert form field and widget for `DateTimePGPSymmetricKeyField` field.""" expected = datetime.now() instance = EncryptedModelFactory.create(datetime_pgp_sym_field=expected) instance.refresh_from_db() # Ensure the PGSQL casting works right payload = { 'datetime_pgp_sym_field': '08/01/2016 14:00' } form = EncryptedForm(payload, instance=instance) self.assertTrue(form.is_valid()) cleaned_data = form.cleaned_data self.assertTrue( cleaned_data['datetime_pgp_sym_field'], datetime(2016, 8, 1, 14, 0, 0) ) def test_pgp_symmetric_key_time(self): """Assert date is save with an `TimePGPSymmetricKeyField` field.""" expected = datetime.now().time() instance = EncryptedModelFactory.create(time_pgp_sym_field=expected) instance.refresh_from_db() # Ensure the PGSQL casting works right self.assertEqual(instance.time_pgp_sym_field, expected) instance = EncryptedModel.objects.get(pk=instance.id) self.assertEqual(instance.time_pgp_sym_field, expected) def test_pgp_pub_key_time(self): """Assert date is save with an `TimePGPPublicKeyField` field.""" expected = datetime.now().time() instance = EncryptedModelFactory.create(time_pgp_pub_field=expected) instance.refresh_from_db() # Ensure the PGSQL casting works right self.assertEqual(instance.time_pgp_pub_field, expected) instance = EncryptedModel.objects.get(pk=instance.id) self.assertEqual(instance.time_pgp_pub_field, expected) def test_pgp_symmetric_key_time_form(self): """Assert form field and widget for `TimePGPSymmetricKeyField` field.""" expected = datetime.now().time() instance = EncryptedModelFactory.create(time_pgp_sym_field=expected) instance.refresh_from_db() # Ensure the PGSQL casting works right payload = { 'time_pgp_sym_field': '{}'.format(expected) } form = EncryptedForm(payload, instance=instance) self.assertTrue(form.is_valid()) cleaned_data = form.cleaned_data self.assertTrue( cleaned_data['time_pgp_sym_field'], expected ) def test_pgp_public_key_time_form(self): """Assert form field and widget for `TimePGPSymmetricKeyField` field.""" expected = datetime.now().time() instance = EncryptedModelFactory.create(time_pgp_pub_field=expected) instance.refresh_from_db() # Ensure the PGSQL casting works right payload = { 'time_pgp_pub_field': '{}'.format(expected) } form = EncryptedForm(payload, instance=instance) self.assertTrue(form.is_valid()) cleaned_data = form.cleaned_data self.assertTrue( cleaned_data['time_pgp_pub_field'], expected ) def test_pgp_public_key_char_field(self): """Test public key CharField.""" expect = 'Peter' EncryptedModelFactory.create(char_pub_field=expect) instance = EncryptedModel.objects.get() self.assertTrue( instance.char_pub_field, expect ) payload = { 'char_pub_field': 'This is beyond 15 max length' } form = EncryptedForm(payload, instance=instance) is_valid = form.is_valid() errors = form.errors.as_data() self.assertFalse(is_valid) self.assertTrue(1, len(errors['char_pub_field'])) def test_pgp_symmetric_key_char_field(self): """Test symmetric key CharField.""" expect = 'Peter' EncryptedModelFactory.create(char_sym_field=expect) instance = EncryptedModel.objects.get() self.assertTrue( instance.char_sym_field, expect ) payload = { 'char_sym_field': 'This is beyond 15 max length' } form = EncryptedForm(payload, instance=instance) is_valid = form.is_valid() errors = form.errors.as_data() self.assertFalse(is_valid) self.assertTrue(1, len(errors['char_sym_field'])) def test_pgp_symmetric_key_date_lookups(self): """Assert lookups `DatePGPSymmetricKeyField` field.""" EncryptedModelFactory.create(date_pgp_sym_field=date(2016, 7, 1)) EncryptedModelFactory.create(date_pgp_sym_field=date(2016, 8, 1)) EncryptedModelFactory.create(date_pgp_sym_field=date(2016, 9, 1)) # EXACT self.assertEqual( 1, EncryptedModel.objects.filter( date_pgp_sym_field__exact=date(2016, 8, 1) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( date_pgp_sym_field__exact=date(2016, 8, 2) ).count() ) # GT self.assertEqual( 1, EncryptedModel.objects.filter( date_pgp_sym_field__gt=date(2016, 8, 1) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( date_pgp_sym_field__gt=date(2016, 10, 1) ).count() ) # GTE self.assertEqual( 2, EncryptedModel.objects.filter( date_pgp_sym_field__gte=date(2016, 8, 1) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( date_pgp_sym_field__gte=date(2016, 10, 1) ).count() ) # LE self.assertEqual( 1, EncryptedModel.objects.filter( date_pgp_sym_field__lt=date(2016, 8, 1) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( date_pgp_sym_field__lt=date(2016, 6, 1) ).count() ) # LTE self.assertEqual( 2, EncryptedModel.objects.filter( date_pgp_sym_field__lte=date(2016, 8, 1) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( date_pgp_sym_field__lte=date(2016, 6, 1) ).count() ) # RANGE self.assertEqual( 3, EncryptedModel.objects.filter( date_pgp_sym_field__range=[date(2016, 6, 1), date(2016, 11, 1)] ).count() ) self.assertEqual( 2, EncryptedModel.objects.filter( date_pgp_sym_field__range=[date(2016, 7, 1), date(2016, 8, 1)] ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( date_pgp_sym_field__range=[date(2016, 10, 2), None] ).count() ) def test_pgp_pub_key_date_lookups(self): """Assert lookups `DatePGPPublicKeyField` field.""" EncryptedModelFactory.create(date_pgp_pub_field=date(2016, 7, 1)) EncryptedModelFactory.create(date_pgp_pub_field=date(2016, 8, 1)) EncryptedModelFactory.create(date_pgp_pub_field=date(2016, 9, 1)) # EXACT self.assertEqual( 1, EncryptedModel.objects.filter( date_pgp_pub_field__exact=date(2016, 8, 1) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( date_pgp_pub_field__exact=date(2016, 8, 2) ).count() ) # GT self.assertEqual( 1, EncryptedModel.objects.filter( date_pgp_pub_field__gt=date(2016, 8, 1) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( date_pgp_pub_field__gt=date(2016, 10, 1) ).count() ) # GTE self.assertEqual( 2, EncryptedModel.objects.filter( date_pgp_pub_field__gte=date(2016, 8, 1) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( date_pgp_pub_field__gte=date(2016, 10, 1) ).count() ) # LE self.assertEqual( 1, EncryptedModel.objects.filter( date_pgp_pub_field__lt=date(2016, 8, 1) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( date_pgp_pub_field__lt=date(2016, 6, 1) ).count() ) # LTE self.assertEqual( 2, EncryptedModel.objects.filter( date_pgp_pub_field__lte=date(2016, 8, 1) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( date_pgp_pub_field__lte=date(2016, 6, 1) ).count() ) # RANGE self.assertEqual( 3, EncryptedModel.objects.filter( date_pgp_pub_field__range=[date(2016, 6, 1), date(2016, 11, 1)] ).count() ) self.assertEqual( 2, EncryptedModel.objects.filter( date_pgp_pub_field__range=[date(2016, 7, 1), date(2016, 8, 1)] ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( date_pgp_pub_field__range=[date(2016, 10, 2), None] ).count() ) def test_pgp_symmetric_key_datetime_lookups(self): """Assert lookups `DateTimePGPSymmetricKeyField` field.""" EncryptedModelFactory.create(datetime_pgp_sym_field=datetime(2016, 7, 1, 0, 0, 0)) EncryptedModelFactory.create(datetime_pgp_sym_field=datetime(2016, 8, 1, 0, 0, 0)) EncryptedModelFactory.create(datetime_pgp_sym_field=datetime(2016, 9, 1, 0, 0, 0)) # EXACT self.assertEqual( 1, EncryptedModel.objects.filter( datetime_pgp_sym_field__exact=datetime(2016, 8, 1, 0, 0, 0) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( datetime_pgp_sym_field__exact=datetime(2016, 8, 1, 0, 0, 1) ).count() ) # GT self.assertEqual( 1, EncryptedModel.objects.filter( datetime_pgp_sym_field__gt=datetime(2016, 8, 1, 0, 0, 0) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( datetime_pgp_sym_field__gt=datetime(2016, 10, 1, 0, 0, 0) ).count() ) # GTE self.assertEqual( 2, EncryptedModel.objects.filter( datetime_pgp_sym_field__gte=datetime(2016, 8, 1, 0, 0, 0) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( datetime_pgp_sym_field__gte=datetime(2016, 10, 1, 0, 0, 0) ).count() ) # LE self.assertEqual( 1, EncryptedModel.objects.filter( datetime_pgp_sym_field__lt=datetime(2016, 8, 1, 0, 0, 0) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( datetime_pgp_sym_field__lt=datetime(2016, 6, 1, 0, 0, 0) ).count() ) # LTE self.assertEqual( 2, EncryptedModel.objects.filter( datetime_pgp_sym_field__lte=datetime(2016, 8, 1, 0, 0, 0) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( datetime_pgp_sym_field__lte=datetime(2016, 6, 1, 0, 0, 0) ).count() ) # RANGE self.assertEqual( 3, EncryptedModel.objects.filter( datetime_pgp_sym_field__range=[ datetime(2016, 6, 1, 0, 0, 0), datetime(2016, 11, 1, 23, 59, 59) ] ).count() ) self.assertEqual( 2, EncryptedModel.objects.filter( datetime_pgp_sym_field__range=[ datetime(2016, 7, 1, 0, 0, 0), datetime(2016, 8, 1, 0, 0, 0) ] ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( datetime_pgp_sym_field__range=[ datetime(2016, 10, 1, 0, 0, 1), None ] ).count() ) def test_pgp_public_key_datetime_lookups(self): """Assert lookups `DateTimePGPPublicKeyField` field.""" EncryptedModelFactory.create(datetime_pgp_pub_field=datetime(2016, 7, 1, 0, 0, 0)) EncryptedModelFactory.create(datetime_pgp_pub_field=datetime(2016, 8, 1, 0, 0, 0)) EncryptedModelFactory.create(datetime_pgp_pub_field=datetime(2016, 9, 1, 0, 0, 0)) # EXACT self.assertEqual( 1, EncryptedModel.objects.filter( datetime_pgp_pub_field__exact=datetime(2016, 8, 1, 0, 0, 0) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( datetime_pgp_pub_field__exact=datetime(2016, 8, 1, 0, 0, 1) ).count() ) # GT self.assertEqual( 1, EncryptedModel.objects.filter( datetime_pgp_pub_field__gt=datetime(2016, 8, 1, 0, 0, 0) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( datetime_pgp_pub_field__gt=datetime(2016, 10, 1, 0, 0, 0) ).count() ) # GTE self.assertEqual( 2, EncryptedModel.objects.filter( datetime_pgp_pub_field__gte=datetime(2016, 8, 1, 0, 0, 0) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( datetime_pgp_pub_field__gte=datetime(2016, 10, 1, 0, 0, 0) ).count() ) # LE self.assertEqual( 1, EncryptedModel.objects.filter( datetime_pgp_pub_field__lt=datetime(2016, 8, 1, 0, 0, 0) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( datetime_pgp_pub_field__lt=datetime(2016, 6, 1, 0, 0, 0) ).count() ) # LTE self.assertEqual( 2, EncryptedModel.objects.filter( datetime_pgp_pub_field__lte=datetime(2016, 8, 1, 0, 0, 0) ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( datetime_pgp_pub_field__lte=datetime(2016, 6, 1, 0, 0, 0) ).count() ) # RANGE self.assertEqual( 3, EncryptedModel.objects.filter( datetime_pgp_pub_field__range=[ datetime(2016, 6, 1, 0, 0, 0), datetime(2016, 11, 1, 23, 59, 59) ] ).count() ) self.assertEqual( 2, EncryptedModel.objects.filter( datetime_pgp_pub_field__range=[ datetime(2016, 7, 1, 0, 0, 0), datetime(2016, 8, 1, 0, 0, 0) ] ).count() ) self.assertEqual( 0, EncryptedModel.objects.filter( datetime_pgp_pub_field__range=[ datetime(2016, 10, 1, 0, 0, 1), None ] ).count() ) def test_decimal_pgp_pub_field(self): """Test DecimalPGPPublicKeyField.""" expected = '100000.99' EncryptedModelFactory.create(decimal_pgp_pub_field=expected) instance = EncryptedModel.objects.get() self.assertIsInstance( instance.decimal_pgp_pub_field, Decimal ) self.assertEqual( instance.decimal_pgp_pub_field, Decimal(expected) ) items = EncryptedModel.objects.filter(decimal_pgp_pub_field__gte='100') self.assertEqual( 1, len(items) ) items = EncryptedModel.objects.filter(decimal_pgp_pub_field__gte='100001.00') self.assertEqual( 0, len(items) ) def test_decimal_pgp_sym_field(self): """Test DecimalPGPSymmetricKeyField.""" expected = '100000.99' EncryptedModelFactory.create(decimal_pgp_sym_field=expected) instance = EncryptedModel.objects.get() self.assertIsInstance( instance.decimal_pgp_sym_field, Decimal ) self.assertEqual( instance.decimal_pgp_sym_field, Decimal(expected) ) items = EncryptedModel.objects.filter(decimal_pgp_sym_field__gte='100') self.assertEqual( 1, len(items) ) items = EncryptedModel.objects.filter(decimal_pgp_sym_field__gte='100001.00') self.assertEqual( 0, len(items) ) def test_pgp_public_key_decimal_form(self): """Assert form field and widget for `DecimalPGPSymmetricKeyField` field.""" expected = '100000.99' instance = EncryptedModelFactory.create(decimal_pgp_pub_field=expected) payload = { 'decimal_pgp_pub_field': expected } form = EncryptedForm(payload, instance=instance) self.assertTrue(form.is_valid()) cleaned_data = form.cleaned_data self.assertTrue( cleaned_data['decimal_pgp_pub_field'], Decimal(expected) ) def test_pgp_symmetric_key_decimal_form(self): """Assert form field and widget for `DecimalPGPSymmetricKeyField` field.""" expected = '100000.99' instance = EncryptedModelFactory.create(decimal_pgp_sym_field=expected) payload = { 'decimal_pgp_sym_field': expected } form = EncryptedForm(payload, instance=instance) self.assertTrue(form.is_valid()) cleaned_data = form.cleaned_data self.assertTrue( cleaned_data['decimal_pgp_sym_field'], Decimal(expected) ) def test_float_pgp_pub_field(self): """Test FloatPGPPublicKeyField.""" expected = 1234.6788 EncryptedModelFactory.create(float_pgp_pub_field=expected) instance = EncryptedModel.objects.get() self.assertIsInstance( instance.float_pgp_pub_field, float ) self.assertEqual( instance.float_pgp_pub_field, expected ) items = EncryptedModel.objects.filter(float_pgp_pub_field__gte='100') self.assertEqual( 1, len(items) ) items = EncryptedModel.objects.filter(float_pgp_pub_field__gte='100001.00') self.assertEqual( 0, len(items) ) def test_float_pgp_sym_field(self): """Test FloatPGPSymmetricKeyField.""" expected = float(1234.6788) EncryptedModelFactory.create(float_pgp_sym_field=expected) instance = EncryptedModel.objects.get() self.assertIsInstance( instance.float_pgp_sym_field, float ) self.assertEqual( instance.float_pgp_sym_field, expected ) items = EncryptedModel.objects.filter(float_pgp_sym_field__gte='100') self.assertEqual( 1, len(items) ) items = EncryptedModel.objects.filter(float_pgp_sym_field__gte='100001.00') self.assertEqual( 0, len(items) ) def test_pgp_public_key_float_form(self): """Assert form field and widget for `FloatPGPPublicKeyField` field.""" expected = '100000.99' instance = EncryptedModelFactory.create(float_pgp_pub_field=expected) payload = { 'float_pgp_pub_field': expected } form = EncryptedForm(payload, instance=instance) self.assertTrue(form.is_valid()) cleaned_data = form.cleaned_data self.assertTrue( cleaned_data['float_pgp_pub_field'], float(expected) ) def test_pgp_symmetric_key_float_form(self): """Assert form field and widget for `FloatPGPSymmetricKeyField` field.""" expected = '100000.99' instance = EncryptedModelFactory.create(float_pgp_sym_field=expected) payload = { 'float_pgp_sym_field': expected } form = EncryptedForm(payload, instance=instance) self.assertTrue(form.is_valid()) cleaned_data = form.cleaned_data self.assertTrue( cleaned_data['float_pgp_sym_field'], float(expected) ) def test_boolean_pgp_pub_field(self): """Test BooleanPGPPublicKeyField.""" expected = True EncryptedModelFactory.create(boolean_pgp_pub_field=expected) instance = EncryptedModel.objects.get() self.assertIsInstance( instance.boolean_pgp_pub_field, bool ) self.assertEqual( instance.boolean_pgp_pub_field, expected ) items = EncryptedModel.objects.filter(boolean_pgp_pub_field=True) self.assertEqual( 1, len(items) ) items = EncryptedModel.objects.filter(boolean_pgp_pub_field=False) self.assertEqual( 0, len(items) ) def test_boolean_pgp_sym_field(self): """Test BooleanPGPSymmetricKeyField.""" expected = False EncryptedModelFactory.create(boolean_pgp_sym_field=expected) instance = EncryptedModel.objects.get() self.assertIsInstance( instance.boolean_pgp_sym_field, bool ) self.assertEqual( instance.boolean_pgp_sym_field, expected ) items = EncryptedModel.objects.filter(boolean_pgp_sym_field=False) self.assertEqual( 1, len(items) ) items = EncryptedModel.objects.filter(float_pgp_sym_field=True) self.assertEqual( 0, len(items) ) def test_pgp_public_key_boolean_form(self): """Assert form field and widget for `BooleanPGPPublicKeyField` field.""" expected = False instance = EncryptedModelFactory.create(boolean_pgp_pub_field=expected) payload = { 'boolean_pgp_pub_field': expected } form = EncryptedForm(payload, instance=instance) self.assertTrue(form.is_valid()) cleaned_data = form.cleaned_data self.assertEqual( cleaned_data['boolean_pgp_pub_field'], expected ) def test_pgp_symmetric_key_boolean_form(self): """Assert form field and widget for `BooleanPGPSymmetricKeyField` field.""" expected = True instance = EncryptedModelFactory.create(boolean_pgp_sym_field=expected) payload = { 'boolean_pgp_sym_field': expected } form = EncryptedForm(payload, instance=instance) self.assertTrue(form.is_valid()) cleaned_data = form.cleaned_data self.assertEqual( cleaned_data['boolean_pgp_sym_field'], expected ) def test_null(self): """Assert `NULL` values are saved.""" instance = EncryptedModel.objects.create() fields = field_names(self.model) fields.remove('id') for field in fields: with self.subTest(instance=instance, field=field): value = getattr(instance, field) self.assertEqual( value, None, msg='Field {}, Value: {}'.format(field, value) ) def test_defer(self): """Test defer() functionality.""" expected = 'bonjour' EncryptedModelFactory.create(pgp_sym_field=expected) instance = self.model.objects.defer('pgp_sym_field').get() # Assert that accessing a field that is in defer() causes a query with self.assertNumQueries(1): temp = instance.pgp_sym_field self.assertEqual(temp, expected) def test_only(self): """Test only() functionality.""" expected = 'bonjour' EncryptedModelFactory.create(pgp_sym_field=expected, pgp_pub_field=expected) instance = self.model.objects.only('pgp_sym_field').get() # Assert that accessing a field in only() does not cause a query with self.assertNumQueries(0): temp = instance.pgp_sym_field self.assertEqual(temp, expected) # Assert that accessing a field not in only() causes a query with self.assertNumQueries(1): temp = instance.pgp_pub_field self.assertEqual(temp, expected) def test_fk_auto_decryption(self): """Test auto decryption of FK when select related is defined.""" expected = 'bonjour' EncryptedModelFactory.create(fk_model__fk_pgp_sym_field=expected) instance = self.model.objects.select_related('fk_model').get() # Assert no additional queries are made to decrypt with self.assertNumQueries(0): temp = instance.fk_model.fk_pgp_sym_field self.assertEqual(temp, expected) def test_get_by_natural_key(self): """Test get_by_natual_key() support.""" expected = 'peter@test.com' EncryptedModelFactory.create(email_pgp_pub_field=expected) instance = self.model.objects.get_by_natural_key(expected) self.assertEqual(instance.email_pgp_pub_field, expected) def test_get_or_create(self): """Test get_or_create() support.""" expected = 'peter@test.com' original = EncryptedModelFactory.create(email_pgp_pub_field=expected) instance, created = self.model.objects.get_or_create( email_pgp_pub_field=expected ) self.assertFalse(created) self.assertEqual(instance.id, original.id) self.assertEqual(instance.email_pgp_pub_field, original.email_pgp_pub_field) instance, created = self.model.objects.get_or_create( email_pgp_pub_field='jessica@test.com' ) self.assertTrue(created) self.assertNotEqual(instance.id, original.id) self.assertEqual(instance.email_pgp_pub_field, 'jessica@test.com') def test_update_or_create(self): """Test update_or_create() support.""" expected = 'peter@test.com' original = EncryptedModelFactory.create( email_pgp_pub_field=expected, pgp_sym_field='Test' ) instance, created = self.model.objects.update_or_create( email_pgp_pub_field='jessica@test.com' ) self.assertTrue(created) self.assertNotEqual(instance.id, original.id) self.assertEqual(instance.email_pgp_pub_field, 'jessica@test.com') instance, created = self.model.objects.update_or_create( email_pgp_pub_field='jessica@test.com', defaults={ 'pgp_sym_field': 'Blue', } ) self.assertFalse(created) self.assertNotEqual(instance.id, original.id) self.assertEqual(instance.pgp_sym_field, 'Blue') def test_aggregates(self): """Test aggregate support.""" EncryptedModelFactory.create(datetime_pgp_sym_field=datetime(2016, 7, 1, 0, 0, 0)) EncryptedModelFactory.create(datetime_pgp_sym_field=datetime(2016, 7, 2, 0, 0, 0)) EncryptedModelFactory.create(datetime_pgp_sym_field=datetime(2016, 8, 1, 0, 0, 0)) EncryptedModelFactory.create(datetime_pgp_sym_field=datetime(2016, 9, 1, 0, 0, 0)) EncryptedModelFactory.create(datetime_pgp_sym_field=datetime(2016, 9, 2, 0, 0, 0)) total_2016 = self.model.objects.aggregate( count=models.Count('datetime_pgp_sym_field') ) self.assertEqual(5, total_2016['count']) total_july = self.model.objects.filter( datetime_pgp_sym_field__range=[ datetime(2016, 7, 1, 0, 0, 0), datetime(2016, 7, 30, 23, 59, 59) ] ).aggregate( count=models.Count('datetime_pgp_sym_field') ) self.assertEqual(2, total_july['count']) total_2016 = self.model.objects.aggregate( count=models.Count('datetime_pgp_sym_field'), min=models.Min('datetime_pgp_sym_field'), max=models.Max('datetime_pgp_sym_field'), ) self.assertEqual(5, total_2016['count']) self.assertEqual(datetime(2016, 7, 1, 0, 0, 0), total_2016['min']) self.assertEqual(datetime(2016, 9, 2, 0, 0, 0), total_2016['max']) total_july = self.model.objects.filter( datetime_pgp_sym_field__range=[ datetime(2016, 7, 1, 0, 0, 0), datetime(2016, 7, 30, 23, 59, 59) ] ).aggregate( count=models.Count('datetime_pgp_sym_field'), min=models.Min('datetime_pgp_sym_field'), max=models.Max('datetime_pgp_sym_field'), ) self.assertEqual(2, total_july['count']) self.assertEqual(datetime(2016, 7, 1, 0, 0, 0), total_july['min']) self.assertEqual(datetime(2016, 7, 2, 0, 0, 0), total_july['max']) def test_distinct(self): """Test distinct support.""" EncryptedModelFactory.create(pgp_sym_field='Paul') EncryptedModelFactory.create(pgp_sym_field='Paul') EncryptedModelFactory.create(pgp_sym_field='Peter') EncryptedModelFactory.create(pgp_sym_field='Peter') EncryptedModelFactory.create(pgp_sym_field='Jessica') EncryptedModelFactory.create(pgp_sym_field='Jessica') items = self.model.objects.filter( pgp_sym_field__startswith='P' ).annotate( _distinct=models.F('pgp_sym_field') ).only( 'id', 'pgp_sym_field', 'fk_model__fk_pgp_sym_field' ).distinct( '_distinct' ) self.assertEqual( 2, len(items) ) # This only works on Django 2.1+ if DJANGO_VERSION[0] >= 2 and DJANGO_VERSION[1] >= 1: items = self.model.objects.filter( pgp_sym_field__startswith='P' ).only( 'id', 'pgp_sym_field', 'fk_model__fk_pgp_sym_field' ).distinct( 'pgp_sym_field' ) self.assertEqual( 2, len(items) ) def test_annotate(self): """Test annotate support.""" efk = EncryptedFKModelFactory.create() EncryptedModelFactory.create(pgp_sym_field='Paul', fk_model=efk) EncryptedModelFactory.create(pgp_sym_field='Peter', fk_model=efk) EncryptedModelFactory.create(pgp_sym_field='Peter', fk_model=efk) EncryptedModelFactory.create(pgp_sym_field='Jessica', fk_model=efk) items = EncryptedFKModel.objects.annotate( name_count=models.Count('encryptedmodel') ) self.assertEqual( 4, items[0].name_count ) items = EncryptedFKModel.objects.filter( encryptedmodel__pgp_sym_field__startswith='J' ).annotate( name_count=models.Count('encryptedmodel') ) self.assertEqual( 1, items[0].name_count ) def test_get_col(self): """Test get_col for related alias.""" related = EncryptedDateTime.objects.create(value=datetime.now()) related_again = EncryptedDateTime.objects.create(value=datetime.now()) RelatedDateTime.objects.create(related=related, related_again=related_again) instance = RelatedDateTime.objects.select_related( 'related', 'related_again' ).get() self.assertIsInstance(instance, RelatedDateTime) def test_char_field_choices(self): """Test CharField choices.""" expected = 1 instance = EncryptedDiff.objects.create( pub_field=expected, sym_field=expected, ) instance.refresh_from_db() # choices always come back as strings self.assertTrue( '{}'.format(expected), instance.pub_field ) self.assertTrue( '{}'.format(expected), instance.sym_field ) def test_write_to_diff_keys(self): """Test writing to diff_keys db which uses different keys.""" expected = 'a' instance = EncryptedDiff.objects.create( pub_field=expected, sym_field=expected, digest_field=expected, hmac_field=expected, ) reset_queries() # Required for Django 1.11 instance = EncryptedDiff.objects.get() self.assertTrue( instance.pub_field, expected ) self.assertTrue( instance.sym_field, expected ) conn = connections['diff_keys'] query = conn.queries[0] self.assertIn( 'djangorocks', str(query) ) self.assertIn( 'lQNTBFvGJCARCAD', str(query) ) instance = EncryptedDiff.objects.get(digest_field__hash_of=expected) self.assertTrue( instance.digest_field, expected ) instance = EncryptedDiff.objects.get(hmac_field__hash_of=expected) self.assertTrue( instance.hmac_field, expected )