Skip to content

Commit 7d7a73a

Browse files
authored
Support multiple databases connections with different crypto keys (#72)
* Added support for different keys based on database (#67) * Updated tests to include digest and hash * Refactored how we get settings * Updated db connection setup * Assert that query actually is using the expected keys * Fixes for django 1.11
1 parent d9d1b52 commit 7d7a73a

File tree

12 files changed

+299
-76
lines changed

12 files changed

+299
-76
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Added new DecimalFields for both public and symmetric key (#64)
66
* Added new FloatFields for both public and symmetric key (#64)
77
* Added new TimeFields for both public and symmetric key (#64)
8+
* Added support for different keys based on database (#67)
89

910
## 2.4.0
1011

README.md

Lines changed: 83 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,100 @@ encrypt and decrypt data for fields.
1616

1717
## Installation
1818

19+
### Install package
20+
1921
```bash
2022
pip install django-pgcrypto-fields
2123
```
2224

23-
In your Django `settings.py`, add `pgcrypto` to `INSTALLED_APPS`:
25+
### Django settings
26+
27+
Our library support different crypto keys for multiple databases by
28+
defining the keys in your `DATABASES` settings.
2429

30+
In `settings.py`:
2531
```python
32+
import os
33+
BASEDIR = os.path.dirname(os.path.dirname(__file__))
34+
PUBLIC_PGP_KEY_PATH = os.path.abspath(os.path.join(BASEDIR, 'public.key'))
35+
PRIVATE_PGP_KEY_PATH = os.path.abspath(os.path.join(BASEDIR, 'private.key'))
36+
37+
# Used by PGPPublicKeyField used by default if not specified by the db
38+
PUBLIC_PGP_KEY = open(PUBLIC_PGP_KEY_PATH).read()
39+
PRIVATE_PGP_KEY = open(PRIVATE_PGP_KEY_PATH).read()
40+
41+
# Used by TextHMACField and PGPSymmetricKeyField if not specified by the db
42+
PGCRYPTO_KEY='ultrasecret'
43+
44+
DIFF_PUBLIC_PGP_KEY_PATH = os.path.abspath(
45+
os.path.join(BASEDIR, 'tests/keys/public_diff.key')
46+
)
47+
DIFF_PRIVATE_PGP_KEY_PATH = os.path.abspath(
48+
os.path.join(BASEDIR, 'tests/keys/private_diff.key')
49+
)
50+
51+
# And add 'pgcrypto' to `INSTALLED_APPS` to create the extension for
52+
# pgcrypto (in a migration).
2653
INSTALLED_APPS = (
2754
'pgcrypto',
28-
# Other apps
55+
# Other installed apps
2956
)
57+
58+
DATABASES = {
59+
# This db will use the default keys above
60+
'default': {
61+
'ENGINE': 'django.db.backends.postgresql_psycopg2',
62+
'NAME': 'pgcryto_fields',
63+
'USER': 'pgcryto_fields',
64+
'PASSWORD': 'xxxx',
65+
'HOST': 'psql.test.com',
66+
'PORT': 5432,
67+
'OPTIONS': {
68+
'sslmode': 'require',
69+
}
70+
},
71+
'diff_keys': {
72+
'ENGINE': 'django.db.backends.postgresql_psycopg2',
73+
'NAME': 'pgcryto_fields_diff',
74+
'USER': 'pgcryto_fields_diff',
75+
'PASSWORD': 'xxxx',
76+
'HOST': 'psqldiff.test.com',
77+
'PORT': 5432,
78+
'OPTIONS': {
79+
'sslmode': 'require',
80+
},
81+
'PGCRYPTO_KEY': 'djangorocks',
82+
'PUBLIC_PGP_KEY': open(DIFF_PUBLIC_PGP_KEY_PATH, 'r').read(),
83+
'PRIVATE_PGP_KEY': open(DIFF_PRIVATE_PGP_KEY_PATH, 'r').read(),
84+
},
85+
}
86+
```
87+
88+
### Generate GPG keys if using Public Key Encryption
89+
90+
The public key is going to encrypt the message and the private key will be
91+
needed to decrypt the content. The following commands have been taken from the
92+
[pgcrypto documentation](http://www.postgresql.org/docs/devel/static/pgcrypto.html)
93+
(see Generating PGP Keys with GnuPG).
94+
95+
Generating a public and a private key (The preferred key type is "DSA and Elgamal".):
96+
97+
```bash
98+
$ gpg --gen-key
99+
$ gpg --list-secret-keys
100+
101+
/home/bob/.gnupg/secring.gpg
102+
---------------------------
103+
sec 2048R/21 2014-10-23
104+
uid Test Key <[email protected]>
105+
ssb 2048R/42 2014-10-23
106+
107+
108+
$ gpg -a --export 42 > public.key
109+
$ gpg -a --export-secret-keys 21 > private.key
30110
```
31111

32-
## Upgrading to 2.4.0 from previous versions
112+
### Upgrading to 2.4.0 from previous versions
33113

34114
The 2.4.0 version of this library received a large rewrite in order to support
35115
auto-decryption when getting encrypted field data as well as the ability to filter
@@ -43,7 +123,6 @@ your application to these items need to be removed as well:
43123
* `admin.PGPAdmin`
44124
* `aggregates.*`
45125

46-
47126
## Fields
48127

49128
`django-pgcrypto-fields` has 3 kinds of fields:
@@ -83,30 +162,6 @@ encrypt the data and a private key to decrypt it.
83162
Public and private keys can be set in settings with `PUBLIC_PGP_KEY` and
84163
`PRIVATE_PGP_KEY`.
85164

86-
##### Generate GPG keys.
87-
88-
The public key is going to encrypt the message and the private key will be
89-
needed to decrypt the content. The following commands have been taken from the
90-
[pgcrypto documentation](http://www.postgresql.org/docs/devel/static/pgcrypto.html)
91-
(see Generating PGP Keys with GnuPG).
92-
93-
Generating a public and a private key:
94-
95-
```bash
96-
$ gpg --gen-key
97-
$ gpg --list-secret-keys
98-
99-
/home/bob/.gnupg/secring.gpg
100-
---------------------------
101-
sec 2048R/21 2014-10-23
102-
uid Test Key <[email protected]>
103-
ssb 2048R/42 2014-10-23
104-
105-
106-
$ gpg -a --export 42 > public.key
107-
$ gpg -a --export-secret-keys 21 > private.key
108-
```
109-
110165
#### Symmetric Key Encryption Fields
111166

112167
Supported PGP symmetric key fields are:
@@ -121,33 +176,6 @@ Supported PGP symmetric key fields are:
121176

122177
Encrypt and decrypt the data with `settings.PGCRYPTO_KEY` which acts like a password.
123178

124-
### Django settings
125-
126-
In `settings.py`:
127-
```python
128-
import os
129-
BASEDIR = os.path.dirname(os.path.dirname(__file__))
130-
PUBLIC_PGP_KEY_PATH = os.path.abspath(os.path.join(BASEDIR, 'public.key'))
131-
PRIVATE_PGP_KEY_PATH = os.path.abspath(os.path.join(BASEDIR, 'private.key'))
132-
133-
134-
# Used by PGPPublicKeyField
135-
PUBLIC_PGP_KEY = open(PUBLIC_PGP_KEY_PATH).read()
136-
PRIVATE_PGP_KEY = open(PRIVATE_PGP_KEY_PATH).read()
137-
138-
# Used by TextHMACField and PGPSymmetricKeyField
139-
PGCRYPTO_KEY='ultrasecret'
140-
141-
142-
# And add 'pgcrypto' to `INSTALLED_APPS` to create the extension for
143-
# pgcrypto (in a migration).
144-
INSTALLED_APPS = (
145-
'pgcrypto',
146-
# Other installed apps
147-
)
148-
149-
```
150-
151179
### Usage
152180

153181
#### Model Definition

pgcrypto/fields.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from django.conf import settings
21
from django.db import models
32

43
from pgcrypto import (
@@ -12,6 +11,7 @@
1211
)
1312
from pgcrypto.mixins import (
1413
DecimalPGPFieldMixin,
14+
get_setting,
1515
HashMixin,
1616
PGPPublicKeyFieldMixin,
1717
PGPSymmetricKeyFieldMixin,
@@ -23,9 +23,9 @@ class TextDigestField(HashMixin, models.TextField):
2323
"""Text digest field for postgres."""
2424
encrypt_sql = DIGEST_SQL
2525

26-
def get_encrypt_sql(self):
26+
def get_encrypt_sql(self, connection):
2727
"""Get encrypt sql."""
28-
return self.encrypt_sql.format(settings.PGCRYPTO_KEY)
28+
return self.encrypt_sql.format(get_setting(connection, 'PGCRYPTO_KEY'))
2929

3030

3131
TextDigestField.register_lookup(HashLookup)

pgcrypto/mixins.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,27 @@ def remove_validators(validators, validator_class):
1616
return [v for v in validators if not isinstance(v, validator_class)]
1717

1818

19+
def get_setting(connection, key):
20+
"""Get key from connection or default to settings."""
21+
if key in connection.settings_dict:
22+
return connection.settings_dict[key]
23+
else:
24+
return getattr(settings, key)
25+
26+
1927
class DecryptedCol(Col):
2028
"""Provide DecryptedCol support without using `extra` sql."""
2129

2230
def __init__(self, alias, target, output_field=None):
2331
"""Init the decryption."""
24-
self.decrypt_sql = target.get_decrypt_sql()
25-
self.cast_sql = target.get_cast_sql()
2632
self.target = target
2733

2834
super(DecryptedCol, self).__init__(alias, target, output_field)
2935

3036
def as_sql(self, compiler, connection):
3137
"""Build SQL with decryption and casting."""
3238
sql, params = super(DecryptedCol, self).as_sql(compiler, connection)
33-
sql = self.decrypt_sql % (sql, self.cast_sql)
39+
sql = self.target.get_decrypt_sql(connection) % (sql, self.target.get_cast_sql())
3440
return sql, params
3541

3642

@@ -67,9 +73,9 @@ def get_placeholder(self, value=None, compiler=None, connection=None):
6773
if value is None or value.startswith('\\x'):
6874
return '%s'
6975

70-
return self.get_encrypt_sql()
76+
return self.get_encrypt_sql(connection)
7177

72-
def get_encrypt_sql(self):
78+
def get_encrypt_sql(self, connection):
7379
"""Get encrypt sql. This may be overidden by some implementations."""
7480
return self.encrypt_sql
7581

@@ -92,15 +98,15 @@ def db_type(self, connection=None):
9298
"""Value stored in the database is hexadecimal."""
9399
return 'bytea'
94100

95-
def get_placeholder(self, value=None, compiler=None, connection=None):
101+
def get_placeholder(self, value, compiler, connection):
96102
"""Tell postgres to encrypt this field using PGP."""
97103
raise NotImplementedError('The `get_placeholder` needs to be implemented.')
98104

99105
def get_cast_sql(self):
100106
"""Get cast sql. This may be overidden by some implementations."""
101107
return self.cast_type
102108

103-
def get_decrypt_sql(self):
109+
def get_decrypt_sql(self, connection):
104110
"""Get decrypt sql."""
105111
raise NotImplementedError('The `get_decrypt_sql` needs to be implemented.')
106112

@@ -138,11 +144,11 @@ class PGPPublicKeyFieldMixin(PGPMixin):
138144

139145
def get_placeholder(self, value=None, compiler=None, connection=None):
140146
"""Tell postgres to encrypt this field using PGP."""
141-
return self.encrypt_sql.format(settings.PUBLIC_PGP_KEY)
147+
return self.encrypt_sql.format(get_setting(connection, 'PUBLIC_PGP_KEY'))
142148

143-
def get_decrypt_sql(self):
149+
def get_decrypt_sql(self, connection):
144150
"""Get decrypt sql."""
145-
return self.decrypt_sql.format(settings.PRIVATE_PGP_KEY)
151+
return self.decrypt_sql.format(get_setting(connection, 'PRIVATE_PGP_KEY'))
146152

147153

148154
class PGPSymmetricKeyFieldMixin(PGPMixin):
@@ -151,13 +157,13 @@ class PGPSymmetricKeyFieldMixin(PGPMixin):
151157
decrypt_sql = PGP_SYM_DECRYPT_SQL
152158
cast_type = 'TEXT'
153159

154-
def get_placeholder(self, value=None, compiler=None, connection=None):
160+
def get_placeholder(self, value, compiler, connection):
155161
"""Tell postgres to encrypt this field using PGP."""
156-
return self.encrypt_sql.format(settings.PGCRYPTO_KEY)
162+
return self.encrypt_sql.format(get_setting(connection, 'PGCRYPTO_KEY'))
157163

158-
def get_decrypt_sql(self):
164+
def get_decrypt_sql(self, connection):
159165
"""Get decrypt sql."""
160-
return self.decrypt_sql.format(settings.PGCRYPTO_KEY)
166+
return self.decrypt_sql.format(get_setting(connection, 'PGCRYPTO_KEY'))
161167

162168

163169
class DecimalPGPFieldMixin:

tests/dbrouters.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class TestRouter(object):
2+
3+
def db_for_read(self, model, **hints):
4+
"""Read from diff_keys."""
5+
if model._meta.app_label == 'diff_keys':
6+
return 'diff_keys'
7+
return 'default'
8+
9+
def db_for_write(self, model, **hints):
10+
"""Write to diff_keys."""
11+
if model._meta.app_label == 'diff_keys':
12+
return 'diff_keys'
13+
return 'default'

tests/default/__init__.py

Whitespace-only changes.

tests/diff_keys/__init__.py

Whitespace-only changes.

tests/diff_keys/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from django.db import models
2+
3+
from pgcrypto import fields
4+
5+
6+
class EncryptedDiff(models.Model):
7+
pub_field = fields.TextPGPPublicKeyField()
8+
sym_field = fields.TextPGPSymmetricKeyField()
9+
digest_field = fields.TextDigestField(blank=True, null=True)
10+
hmac_field = fields.TextHMACField(blank=True, null=True)
11+
12+
class Meta:
13+
"""Sets up the meta for the test model."""
14+
app_label = 'diff_keys'

tests/keys/private_diff.key

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
-----BEGIN PGP PRIVATE KEY BLOCK-----
2+
Version: GnuPG v1
3+
4+
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
39+
zhkqb4tHw1kFgzXsT9FuzF+r
40+
=JjTF
41+
-----END PGP PRIVATE KEY BLOCK-----

0 commit comments

Comments
 (0)