master 68d60e173e22 cached
34 files
109.4 KB
31.5k tokens
140 symbols
1 requests
Download .txt
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 <charlie@meshy.co.uk>
* Kévin Etienne <etienne.kevin@gmail.com>
* Peter J. Farrell <pjf@maepub.com>
* Max Peterson <mail@maxpeterson.co.uk>

## Contributors

* Esther Onfroy <https://twitter.com/U039b>


================================================
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 <example@example.com>
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')
[<MyModel: MyModel object>]
>>> my_model = MyModel.objects.filter(hmac_field__hash_of='value')
[<MyModel: MyModel object>]

```

## 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
        )
Download .txt
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
Download .txt
SYMBOL INDEX (140 symbols across 11 files)

FILE: pgcrypto/fields.py
  class TextDigestField (line 21) | class TextDigestField(HashMixin, models.TextField):
    method get_encrypt_sql (line 25) | def get_encrypt_sql(self, connection):
  class TextHMACField (line 33) | class TextHMACField(HashMixin, models.TextField):
  class EmailPGPPublicKeyField (line 41) | class EmailPGPPublicKeyField(PGPPublicKeyFieldMixin, models.EmailField):
  class IntegerPGPPublicKeyField (line 45) | class IntegerPGPPublicKeyField(PGPPublicKeyFieldMixin, models.IntegerFie...
  class BigIntegerPGPPublicKeyField (line 51) | class BigIntegerPGPPublicKeyField(PGPPublicKeyFieldMixin, models.Integer...
  class TextPGPPublicKeyField (line 57) | class TextPGPPublicKeyField(PGPPublicKeyFieldMixin, models.TextField):
  class CharPGPPublicKeyField (line 61) | class CharPGPPublicKeyField(PGPPublicKeyFieldMixin, models.CharField):
  class DatePGPPublicKeyField (line 65) | class DatePGPPublicKeyField(PGPPublicKeyFieldMixin, models.DateField):
  class DateTimePGPPublicKeyField (line 71) | class DateTimePGPPublicKeyField(PGPPublicKeyFieldMixin, models.DateTimeF...
  class BooleanPGPPublicKeyField (line 77) | class BooleanPGPPublicKeyField(PGPPublicKeyFieldMixin, models.BooleanFie...
  class EmailPGPSymmetricKeyField (line 83) | class EmailPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.EmailF...
  class IntegerPGPSymmetricKeyField (line 87) | class IntegerPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.Inte...
  class BigIntegerPGPSymmetricKeyField (line 93) | class BigIntegerPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.I...
  class TextPGPSymmetricKeyField (line 99) | class TextPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.TextFie...
  class CharPGPSymmetricKeyField (line 103) | class CharPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.CharFie...
  class DatePGPSymmetricKeyField (line 107) | class DatePGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.DateFie...
  class DateTimePGPSymmetricKeyField (line 113) | class DateTimePGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.Dat...
  class BooleanPGPSymmetricKeyField (line 119) | class BooleanPGPSymmetricKeyField(PGPPublicKeyFieldMixin, models.Boolean...
  class DecimalPGPPublicKeyField (line 125) | class DecimalPGPPublicKeyField(DecimalPGPFieldMixin,
  class DecimalPGPSymmetricKeyField (line 130) | class DecimalPGPSymmetricKeyField(DecimalPGPFieldMixin,
  class FloatPGPPublicKeyField (line 135) | class FloatPGPPublicKeyField(PGPPublicKeyFieldMixin, models.FloatField):
  class FloatPGPSymmetricKeyField (line 141) | class FloatPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.FloatF...
  class TimePGPPublicKeyField (line 147) | class TimePGPPublicKeyField(PGPPublicKeyFieldMixin, models.TimeField):
  class TimePGPSymmetricKeyField (line 153) | class TimePGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.TimeFie...

FILE: pgcrypto/lookups.py
  class HashLookup (line 4) | class HashLookup(Lookup):
    method as_sql (line 12) | def as_sql(self, qn, connection):

FILE: pgcrypto/migrations/0001_add_pgcrypto_extension.py
  class Migration (line 5) | class Migration(migrations.Migration):

FILE: pgcrypto/mixins.py
  function get_setting (line 13) | def get_setting(connection, key):
  class DecryptedCol (line 21) | class DecryptedCol(Col):
    method __init__ (line 24) | def __init__(self, alias, target, output_field=None):
    method as_sql (line 30) | def as_sql(self, compiler, connection):
  class HashMixin (line 37) | class HashMixin:
    method __init__ (line 44) | def __init__(self, original=None, *args, **kwargs):
    method pre_save (line 50) | def pre_save(self, model_instance, add):
    method get_placeholder (line 58) | def get_placeholder(self, value=None, compiler=None, connection=None):
    method get_encrypt_sql (line 72) | def get_encrypt_sql(self, connection):
  class PGPMixin (line 77) | class PGPMixin:
    method __init__ (line 86) | def __init__(self, *args, **kwargs):
    method db_type (line 90) | def db_type(self, connection=None):
    method get_placeholder (line 94) | def get_placeholder(self, value, compiler, connection):
    method get_cast_sql (line 98) | def get_cast_sql(self):
    method get_decrypt_sql (line 102) | def get_decrypt_sql(self, connection):
    method get_col (line 106) | def get_col(self, alias, output_field=None):
    method cached_col (line 120) | def cached_col(self):
  class PGPPublicKeyFieldMixin (line 128) | class PGPPublicKeyFieldMixin(PGPMixin):
    method get_placeholder (line 134) | def get_placeholder(self, value=None, compiler=None, connection=None):
    method get_decrypt_sql (line 138) | def get_decrypt_sql(self, connection):
  class PGPSymmetricKeyFieldMixin (line 143) | class PGPSymmetricKeyFieldMixin(PGPMixin):
    method get_placeholder (line 149) | def get_placeholder(self, value, compiler, connection):
    method get_decrypt_sql (line 153) | def get_decrypt_sql(self, connection):
  class DecimalPGPFieldMixin (line 158) | class DecimalPGPFieldMixin:
    method get_cast_sql (line 162) | def get_cast_sql(self):

FILE: tests/dbrouters.py
  class TestRouter (line 1) | class TestRouter(object):
    method db_for_read (line 3) | def db_for_read(self, model, **hints):
    method db_for_write (line 9) | def db_for_write(self, model, **hints):

FILE: tests/diff_keys/models.py
  class EncryptedDiff (line 6) | class EncryptedDiff(models.Model):
    class Meta (line 18) | class Meta:

FILE: tests/factories.py
  class EncryptedFKModelFactory (line 9) | class EncryptedFKModelFactory(factory.django.DjangoModelFactory):
    class Meta (line 13) | class Meta:
  class EncryptedModelFactory (line 18) | class EncryptedModelFactory(factory.django.DjangoModelFactory):
    class Meta (line 47) | class Meta:

FILE: tests/forms.py
  class EncryptedForm (line 6) | class EncryptedForm(forms.ModelForm):
    class Meta (line 8) | class Meta:

FILE: tests/models.py
  class EncryptedFKModel (line 6) | class EncryptedFKModel(models.Model):
    class Meta (line 10) | class Meta:
  class EncryptedModelManager (line 15) | class EncryptedModelManager(models.Manager):
    method get_by_natural_key (line 17) | def get_by_natural_key(self, email_pgp_pub_field):
  class EncryptedModel (line 22) | class EncryptedModel(models.Model):
    class Meta (line 68) | class Meta:
  class EncryptedDateTime (line 73) | class EncryptedDateTime(models.Model):
  class RelatedDateTime (line 77) | class RelatedDateTime(models.Model):

FILE: tests/run.py
  class TestRunner (line 59) | class TestRunner(ColourRunnerMixin, DiscoverRunner):

FILE: tests/test_fields.py
  class TestTextFieldHash (line 32) | class TestTextFieldHash(TestCase):
    method test_get_placeholder (line 34) | def test_get_placeholder(self):
  class TestPGPMixin (line 42) | class TestPGPMixin(TestCase):
    method test_check (line 45) | def test_check(self):
    method test_db_type (line 52) | def test_db_type(self):
  class TestEmailPGPMixin (line 59) | class TestEmailPGPMixin(TestCase):
    method test_max_length_validator (line 61) | def test_max_length_validator(self):
  class TestEncryptedTextFieldModel (line 69) | class TestEncryptedTextFieldModel(TestCase):
    method test_fields (line 77) | def test_fields(self):
    method test_value_returned_is_not_bytea (line 112) | def test_value_returned_is_not_bytea(self):
    method test_value_query (line 134) | def test_value_query(self):
    method test_value_pgp_pub (line 147) | def test_value_pgp_pub(self):
    method test_value_pgp_pub_multiple (line 157) | def test_value_pgp_pub_multiple(self):
    method test_value_pgp_sym (line 168) | def test_value_pgp_sym(self):
    method test_instance_not_saved (line 178) | def test_instance_not_saved(self):
    method test_decrypt_filter (line 185) | def test_decrypt_filter(self):
    method test_digest_lookup (line 213) | def test_digest_lookup(self):
    method test_digest_with_original_lookup (line 223) | def test_digest_with_original_lookup(self):
    method test_hmac_lookup (line 234) | def test_hmac_lookup(self):
    method test_hmac_with_original_lookup (line 243) | def test_hmac_with_original_lookup(self):
    method test_default_lookup (line 252) | def test_default_lookup(self):
    method test_update_attribute_digest_field (line 257) | def test_update_attribute_digest_field(self):
    method test_update_attribute_hmac_field (line 267) | def test_update_attribute_hmac_field(self):
    method test_update_attribute_pgp_pub_field (line 277) | def test_update_attribute_pgp_pub_field(self):
    method test_update_attribute_pgp_sym_field (line 287) | def test_update_attribute_pgp_sym_field(self):
    method test_update_one_attribute (line 297) | def test_update_one_attribute(self):
    method test_pgp_public_key_negative_number (line 321) | def test_pgp_public_key_negative_number(self):
    method test_pgp_symmetric_key_negative_number (line 338) | def test_pgp_symmetric_key_negative_number(self):
    method test_pgp_symmetric_key_date (line 355) | def test_pgp_symmetric_key_date(self):
    method test_pgp_pub_key_date (line 367) | def test_pgp_pub_key_date(self):
    method test_pgp_symmetric_key_date_form (line 379) | def test_pgp_symmetric_key_date_form(self):
    method test_pgp_symmetric_key_datetime_form (line 399) | def test_pgp_symmetric_key_datetime_form(self):
    method test_pgp_symmetric_key_time (line 419) | def test_pgp_symmetric_key_time(self):
    method test_pgp_pub_key_time (line 431) | def test_pgp_pub_key_time(self):
    method test_pgp_symmetric_key_time_form (line 443) | def test_pgp_symmetric_key_time_form(self):
    method test_pgp_public_key_time_form (line 463) | def test_pgp_public_key_time_form(self):
    method test_pgp_public_key_char_field (line 483) | def test_pgp_public_key_char_field(self):
    method test_pgp_symmetric_key_char_field (line 505) | def test_pgp_symmetric_key_char_field(self):
    method test_pgp_symmetric_key_date_lookups (line 527) | def test_pgp_symmetric_key_date_lookups(self):
    method test_pgp_pub_key_date_lookups (line 625) | def test_pgp_pub_key_date_lookups(self):
    method test_pgp_symmetric_key_datetime_lookups (line 723) | def test_pgp_symmetric_key_datetime_lookups(self):
    method test_pgp_public_key_datetime_lookups (line 830) | def test_pgp_public_key_datetime_lookups(self):
    method test_decimal_pgp_pub_field (line 937) | def test_decimal_pgp_pub_field(self):
    method test_decimal_pgp_sym_field (line 968) | def test_decimal_pgp_sym_field(self):
    method test_pgp_public_key_decimal_form (line 999) | def test_pgp_public_key_decimal_form(self):
    method test_pgp_symmetric_key_decimal_form (line 1018) | def test_pgp_symmetric_key_decimal_form(self):
    method test_float_pgp_pub_field (line 1037) | def test_float_pgp_pub_field(self):
    method test_float_pgp_sym_field (line 1068) | def test_float_pgp_sym_field(self):
    method test_pgp_public_key_float_form (line 1099) | def test_pgp_public_key_float_form(self):
    method test_pgp_symmetric_key_float_form (line 1118) | def test_pgp_symmetric_key_float_form(self):
    method test_boolean_pgp_pub_field (line 1137) | def test_boolean_pgp_pub_field(self):
    method test_boolean_pgp_sym_field (line 1168) | def test_boolean_pgp_sym_field(self):
    method test_pgp_public_key_boolean_form (line 1199) | def test_pgp_public_key_boolean_form(self):
    method test_pgp_symmetric_key_boolean_form (line 1218) | def test_pgp_symmetric_key_boolean_form(self):
    method test_null (line 1237) | def test_null(self):
    method test_defer (line 1252) | def test_defer(self):
    method test_only (line 1264) | def test_only(self):
    method test_fk_auto_decryption (line 1282) | def test_fk_auto_decryption(self):
    method test_get_by_natural_key (line 1294) | def test_get_by_natural_key(self):
    method test_get_or_create (line 1303) | def test_get_or_create(self):
    method test_update_or_create (line 1324) | def test_update_or_create(self):
    method test_aggregates (line 1351) | def test_aggregates(self):
    method test_distinct (line 1401) | def test_distinct(self):
    method test_annotate (line 1440) | def test_annotate(self):
    method test_get_col (line 1468) | def test_get_col(self):
    method test_char_field_choices (line 1481) | def test_char_field_choices(self):
    method test_write_to_diff_keys (line 1501) | def test_write_to_diff_keys(self):
Condensed preview — 34 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (118K chars).
[
  {
    "path": ".gitignore",
    "chars": 718,
    "preview": "### Python template\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n # C extensions\n*.so\n # Di"
  },
  {
    "path": ".travis.yml",
    "chars": 662,
    "preview": "language: python\nsudo: required\ndist: xenial\npython:\n  - 3.6\n  - 3.7\n  - 3.8\n  - 3.9-dev\nscript: make test-coveralls\nnot"
  },
  {
    "path": "AUTHORS.md",
    "chars": 252,
    "preview": "# Credits\n\n\n## Development Lead\n\n* Charlie Denton <charlie@meshy.co.uk>\n* Kévin Etienne <etienne.kevin@gmail.com>\n* Pete"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 4727,
    "preview": "# CHANGELOG\n\n## Master (unreleased)\n\n* Added support for BooleanFields (#325)\n\n## 2.6.0\n\n* Added support for Django 3.1."
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 3570,
    "preview": "# Contributing to Django-PGCrypto-Fields\n\nContributions are welcome, and they are greatly appreciated! Every little bit\n"
  },
  {
    "path": "LICENSE",
    "chars": 1308,
    "preview": "Copyright (c) 2014 Incuna Ltd\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without m"
  },
  {
    "path": "Makefile",
    "chars": 1933,
    "preview": ".PHONY: clean-build lint help\n.DEFAULT_GOAL := help\n\ndefine BROWSER_PYSCRIPT\nimport os, webbrowser, sys\n\ntry:\n\tfrom urll"
  },
  {
    "path": "README.md",
    "chars": 16230,
    "preview": "# django-pgcrypto-fields\n\n[![Latest Release](https://img.shields.io/pypi/v/django-pgcrypto-fields.svg)](https://pypi.org"
  },
  {
    "path": "pgcrypto/__init__.py",
    "chars": 476,
    "preview": "DIGEST_SQL = \"digest(%s, 'sha512')\"\nHMAC_SQL = \"hmac(%s, '{}', 'sha512')\"\n\nPGP_PUB_ENCRYPT_SQL_WITH_NULLIF = \"pgp_pub_en"
  },
  {
    "path": "pgcrypto/fields.py",
    "chars": 5029,
    "preview": "from django.db import models\n\nfrom pgcrypto import (\n    DIGEST_SQL,\n    HMAC_SQL,\n    PGP_PUB_ENCRYPT_SQL_WITH_NULLIF,\n"
  },
  {
    "path": "pgcrypto/lookups.py",
    "chars": 737,
    "preview": "from django.db.models.lookups import Lookup\n\n\nclass HashLookup(Lookup):\n    \"\"\"Lookup to filter hashed values.\n\n    `Has"
  },
  {
    "path": "pgcrypto/migrations/0001_add_pgcrypto_extension.py",
    "chars": 223,
    "preview": "from django.contrib.postgres.operations import CreateExtension\nfrom django.db import migrations\n\n\nclass Migration(migrat"
  },
  {
    "path": "pgcrypto/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "pgcrypto/mixins.py",
    "chars": 5540,
    "preview": "from django.conf import settings\nfrom django.db.models.expressions import Col\nfrom django.utils.functional import cached"
  },
  {
    "path": "pgcrypto/models.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "requirements.txt",
    "chars": 23,
    "preview": "-e .\ndjango>=1.11,<3.2\n"
  },
  {
    "path": "requirements_dev.txt",
    "chars": 300,
    "preview": "colour-runner==0.1.1\ncoveralls==3.1.0\ncoverage==5.5\ndj-database-url==0.5.0\nfactory-boy==3.2.0\nflake8-docstrings==1.6.0\nf"
  },
  {
    "path": "setup.cfg",
    "chars": 351,
    "preview": "[flake8]\nmax-line-length = 90\nmax-complexity = 10\nexclude =\n    *migrations*,\n    venv\nignore = D100,D101,D104,D203,D204"
  },
  {
    "path": "setup.py",
    "chars": 2135,
    "preview": "import sys\n\nfrom setuptools import find_packages, setup\n\nCURRENT_PYTHON = sys.version_info[:2]\nREQUIRED_PYTHON = (3, 6)\n"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/dbrouters.py",
    "chars": 394,
    "preview": "class TestRouter(object):\n\n    def db_for_read(self, model, **hints):\n        \"\"\"Read from diff_keys.\"\"\"\n        if mode"
  },
  {
    "path": "tests/default/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/diff_keys/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/diff_keys/models.py",
    "chars": 675,
    "preview": "from django.db import models\n\nfrom pgcrypto import fields\n\n\nclass EncryptedDiff(models.Model):\n    CHOICES = (\n        ("
  },
  {
    "path": "tests/factories.py",
    "chars": 1669,
    "preview": "from datetime import date, datetime\nfrom decimal import Decimal\n\nimport factory\n\nfrom .models import EncryptedFKModel, E"
  },
  {
    "path": "tests/forms.py",
    "chars": 577,
    "preview": "from django import forms\n\nfrom .models import EncryptedModel\n\n\nclass EncryptedForm(forms.ModelForm):\n    \"\"\"Test for Enc"
  },
  {
    "path": "tests/keys/README.md",
    "chars": 132,
    "preview": "Dragon ahead, do not use these keys. They have been generated for testing purpose.\nYou should never expose your private/"
  },
  {
    "path": "tests/keys/private.key",
    "chars": 3463,
    "preview": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: GnuPG v1\n\nlQOYBFRJDkwBCADVJDHy7al1P8urXWDkTSMfb6vGivD6vW0N2dDFixxKHDlOej5"
  },
  {
    "path": "tests/keys/private_diff.key",
    "chars": 2398,
    "preview": "-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: GnuPG v1\n\nlQNTBFvGJCARCADxYnKHEuntDpQ4URPYhQwBSEDlEJpymFo4ch56KBcEuJCUwRC"
  },
  {
    "path": "tests/keys/public.key",
    "chars": 1698,
    "preview": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: GnuPG v1\n\nmQENBFRJDkwBCADVJDHy7al1P8urXWDkTSMfb6vGivD6vW0N2dDFixxKHDlOej52"
  },
  {
    "path": "tests/keys/public_diff.key",
    "chars": 2282,
    "preview": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: GnuPG v1\n\nmQMuBFvGJCARCADxYnKHEuntDpQ4URPYhQwBSEDlEJpymFo4ch56KBcEuJCUwRCt"
  },
  {
    "path": "tests/models.py",
    "chars": 3727,
    "preview": "from django.db import models\n\nfrom pgcrypto import fields\n\n\nclass EncryptedFKModel(models.Model):\n    \"\"\"Dummy model use"
  },
  {
    "path": "tests/run.py",
    "chars": 1800,
    "preview": "#! /usr/bin/env python\n\"\"\"From http://stackoverflow.com/a/12260597/400691.\"\"\"\nimport os\nimport sys\n\nimport dj_database_u"
  },
  {
    "path": "tests/test_fields.py",
    "chars": 49020,
    "preview": "from datetime import date, datetime\nfrom decimal import Decimal\nfrom unittest.mock import MagicMock\n\nfrom django import "
  }
]

About this extraction

This page contains the full source code of the incuna/django-pgcrypto-fields GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 34 files (109.4 KB), approximately 31.5k tokens, and a symbol index with 140 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!