Skip to content

Commit 5410952

Browse files
authored
Support CharField for Django parity and correct Form fields (#74)
* * Added support for different keys based on database (#67) * Updated tests to include digest and hash * Completed #73 * Adds additional tests to #73 * Code clean up * Merged in master * Added 2nd db to travis (it doesn't fail but this is more precise)
1 parent 7d7a73a commit 5410952

File tree

8 files changed

+93
-24
lines changed

8 files changed

+93
-24
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ services:
1212
- postgresql
1313
before_script:
1414
- psql -c 'CREATE DATABASE pgcrypto_fields' -U postgres
15+
- psql -c 'CREATE DATABASE pgcrypto_fields_diff' -U postgres
1516
install:
1617
- pip install -e .
1718
- pip install -r requirements_dev.txt

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ the data and also changing the hash to match.
147147
#### Public Key Encryption Fields
148148

149149
Supported PGP public key fields are:
150+
- `CharPGPPublicKeyField`
150151
- `EmailPGPPublicKeyField`
151152
- `IntegerPGPPublicKeyField`
152153
- `TextPGPPublicKeyField`
@@ -165,6 +166,7 @@ Public and private keys can be set in settings with `PUBLIC_PGP_KEY` and
165166
#### Symmetric Key Encryption Fields
166167

167168
Supported PGP symmetric key fields are:
169+
- `CharPGPSymmetricKeyField`
168170
- `EmailPGPSymmetricKeyField`
169171
- `IntegerPGPSymmetricKeyField`
170172
- `TextPGPSymmetricKeyField`

pgcrypto/fields.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
HashMixin,
1616
PGPPublicKeyFieldMixin,
1717
PGPSymmetricKeyFieldMixin,
18-
RemoveMaxLengthValidatorMixin,
1918
)
2019

2120

@@ -39,8 +38,7 @@ class TextHMACField(HashMixin, models.TextField):
3938
TextHMACField.register_lookup(HashLookup)
4039

4140

42-
class EmailPGPPublicKeyField(RemoveMaxLengthValidatorMixin,
43-
PGPSymmetricKeyFieldMixin, models.EmailField):
41+
class EmailPGPPublicKeyField(PGPSymmetricKeyFieldMixin, models.EmailField):
4442
"""Email PGP public key encrypted field."""
4543

4644

@@ -54,6 +52,10 @@ class TextPGPPublicKeyField(PGPPublicKeyFieldMixin, models.TextField):
5452
"""Text PGP public key encrypted field."""
5553

5654

55+
class CharPGPPublicKeyField(PGPPublicKeyFieldMixin, models.CharField):
56+
"""Char PGP public key encrypted field."""
57+
58+
5759
class DatePGPPublicKeyField(PGPPublicKeyFieldMixin, models.DateField):
5860
"""Date PGP public key encrypted field for postgres."""
5961
encrypt_sql = PGP_PUB_ENCRYPT_SQL_WITH_NULLIF
@@ -66,8 +68,7 @@ class DateTimePGPPublicKeyField(PGPPublicKeyFieldMixin, models.DateTimeField):
6668
cast_type = 'TIMESTAMP'
6769

6870

69-
class EmailPGPSymmetricKeyField(RemoveMaxLengthValidatorMixin,
70-
PGPSymmetricKeyFieldMixin, models.EmailField):
71+
class EmailPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.EmailField):
7172
"""Email PGP symmetric key encrypted field."""
7273

7374

@@ -81,6 +82,10 @@ class TextPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.TextField):
8182
"""Text PGP symmetric key encrypted field for postgres."""
8283

8384

85+
class CharPGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.CharField):
86+
"""Char PGP symmetric key encrypted field for postgres."""
87+
88+
8489
class DatePGPSymmetricKeyField(PGPSymmetricKeyFieldMixin, models.DateField):
8590
"""Date PGP symmetric key encrypted field for postgres."""
8691
encrypt_sql = PGP_SYM_ENCRYPT_SQL_WITH_NULLIF

pgcrypto/mixins.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ class PGPMixin:
9191

9292
def __init__(self, *args, **kwargs):
9393
"""`max_length` should be set to None as encrypted text size is variable."""
94-
kwargs['max_length'] = None
9594
super().__init__(*args, **kwargs)
9695

9796
def db_type(self, connection=None):
@@ -110,10 +109,6 @@ def get_decrypt_sql(self, connection):
110109
"""Get decrypt sql."""
111110
raise NotImplementedError('The `get_decrypt_sql` needs to be implemented.')
112111

113-
def _check_max_length_attribute(self, **kwargs):
114-
"""Override `_check_max_length_attribute` to remove check on max_length."""
115-
return []
116-
117112
def get_col(self, alias, output_field=None):
118113
"""Get the decryption for col."""
119114
if output_field is None:

tests/diff_keys/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@
44

55

66
class EncryptedDiff(models.Model):
7-
pub_field = fields.TextPGPPublicKeyField()
8-
sym_field = fields.TextPGPSymmetricKeyField()
7+
CHOICES = (
8+
('a', 'a'),
9+
(1, '1'),
10+
)
11+
pub_field = fields.CharPGPPublicKeyField(blank=True, null=True,
12+
choices=CHOICES, max_length=1)
13+
sym_field = fields.CharPGPSymmetricKeyField(blank=True, null=True,
14+
choices=CHOICES, max_length=1)
915
digest_field = fields.TextDigestField(blank=True, null=True)
1016
hmac_field = fields.TextHMACField(blank=True, null=True)
1117

tests/factories.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class EncryptedModelFactory(factory.DjangoModelFactory):
2424
email_pgp_pub_field = factory.Sequence('email{}@public.key'.format)
2525
integer_pgp_pub_field = 42
2626
pgp_pub_field = factory.Sequence('Text with public key {}'.format)
27+
char_pub_field = factory.Sequence('Text {}'.format)
2728

2829
date_pgp_pub_field = date.today()
2930
datetime_pgp_pub_field = datetime.now()
@@ -32,6 +33,7 @@ class EncryptedModelFactory(factory.DjangoModelFactory):
3233
email_pgp_sym_field = factory.Sequence('email{}@symmetric.key'.format)
3334
integer_pgp_sym_field = 43
3435
pgp_sym_field = factory.Sequence('Text with symmetric key {}'.format)
36+
char_sym_field = factory.Sequence('Text {}'.format)
3537

3638
date_pgp_sym_field = date.today()
3739
datetime_pgp_sym_field = datetime.now()

tests/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class EncryptedModel(models.Model):
3232
unique=True)
3333
integer_pgp_pub_field = fields.IntegerPGPPublicKeyField(blank=True, null=True)
3434
pgp_pub_field = fields.TextPGPPublicKeyField(blank=True, null=True)
35+
char_pub_field = fields.CharPGPPublicKeyField(blank=True, null=True, max_length=15)
3536
date_pgp_pub_field = fields.DatePGPPublicKeyField(blank=True, null=True)
3637
datetime_pgp_pub_field = fields.DateTimePGPPublicKeyField(blank=True, null=True)
3738
time_pgp_pub_field = fields.TimePGPPublicKeyField(blank=True, null=True)
@@ -43,6 +44,7 @@ class EncryptedModel(models.Model):
4344
email_pgp_sym_field = fields.EmailPGPSymmetricKeyField(blank=True, null=True)
4445
integer_pgp_sym_field = fields.IntegerPGPSymmetricKeyField(blank=True, null=True)
4546
pgp_sym_field = fields.TextPGPSymmetricKeyField(blank=True, null=True)
47+
char_sym_field = fields.CharPGPPublicKeyField(blank=True, null=True, max_length=15)
4648
date_pgp_sym_field = fields.DatePGPSymmetricKeyField(blank=True, null=True)
4749
datetime_pgp_sym_field = fields.DateTimePGPSymmetricKeyField(blank=True, null=True)
4850
time_pgp_sym_field = fields.TimePGPSymmetricKeyField(blank=True, null=True)

tests/test_fields.py

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,6 @@ def test_check(self):
4747
field.model = MagicMock()
4848
self.assertEqual(field(name='field').check(), [])
4949

50-
def test_max_length(self):
51-
"""Assert `max_length` is ignored."""
52-
for field in PGP_FIELDS:
53-
with self.subTest(field=field):
54-
self.assertEqual(field(max_length=42).max_length, None)
55-
5650
def test_db_type(self):
5751
"""Check db_type is `bytea`."""
5852
for field in PGP_FIELDS:
@@ -90,10 +84,12 @@ def test_fields(self):
9084
'email_pgp_pub_field',
9185
'integer_pgp_pub_field',
9286
'pgp_pub_field',
87+
'char_pub_field',
9388
'decimal_pgp_pub_field',
9489
'email_pgp_sym_field',
9590
'integer_pgp_sym_field',
9691
'pgp_sym_field',
92+
'char_sym_field',
9793
'date_pgp_sym_field',
9894
'datetime_pgp_sym_field',
9995
'time_pgp_sym_field',
@@ -456,6 +452,50 @@ def test_pgp_public_key_time_form(self):
456452
expected
457453
)
458454

455+
def test_pgp_public_key_char_field(self):
456+
"""Test public key CharField."""
457+
expect = 'Peter'
458+
EncryptedModelFactory.create(char_pub_field=expect)
459+
460+
instance = EncryptedModel.objects.get()
461+
462+
self.assertTrue(
463+
instance.char_pub_field,
464+
expect
465+
)
466+
467+
payload = {
468+
'char_pub_field': 'This is beyond 15 max length'
469+
}
470+
471+
form = EncryptedForm(payload, instance=instance)
472+
is_valid = form.is_valid()
473+
errors = form.errors.as_data()
474+
self.assertFalse(is_valid)
475+
self.assertTrue(1, len(errors['char_pub_field']))
476+
477+
def test_pgp_symmetric_key_char_field(self):
478+
"""Test symmetric key CharField."""
479+
expect = 'Peter'
480+
EncryptedModelFactory.create(char_sym_field=expect)
481+
482+
instance = EncryptedModel.objects.get()
483+
484+
self.assertTrue(
485+
instance.char_sym_field,
486+
expect
487+
)
488+
489+
payload = {
490+
'char_sym_field': 'This is beyond 15 max length'
491+
}
492+
493+
form = EncryptedForm(payload, instance=instance)
494+
is_valid = form.is_valid()
495+
errors = form.errors.as_data()
496+
self.assertFalse(is_valid)
497+
self.assertTrue(1, len(errors['char_sym_field']))
498+
459499
def test_pgp_symmetric_key_date_lookups(self):
460500
"""Assert lookups `DatePGPSymmetricKeyField` field."""
461501
EncryptedModelFactory.create(date_pgp_sym_field=date(2016, 7, 1))
@@ -1304,19 +1344,35 @@ def test_get_col(self):
13041344

13051345
RelatedDateTime.objects.create(related=related, related_again=related_again)
13061346

1307-
instance = RelatedDateTime.objects.select_related(
1308-
'related', 'related_again'
1309-
)
1310-
13111347
instance = RelatedDateTime.objects.select_related(
13121348
'related', 'related_again'
13131349
).get()
13141350

13151351
self.assertIsInstance(instance, RelatedDateTime)
13161352

1353+
def test_char_field_choices(self):
1354+
"""Test CharField choices."""
1355+
expected = 1
1356+
instance = EncryptedDiff.objects.create(
1357+
pub_field=expected,
1358+
sym_field=expected,
1359+
)
1360+
instance.refresh_from_db()
1361+
1362+
# choices always come back as strings
1363+
self.assertTrue(
1364+
'{}'.format(expected),
1365+
instance.pub_field
1366+
)
1367+
1368+
self.assertTrue(
1369+
'{}'.format(expected),
1370+
instance.sym_field
1371+
)
1372+
13171373
def test_write_to_diff_keys(self):
13181374
"""Test writing to diff_keys db which uses different keys."""
1319-
expected = 'bonjour'
1375+
expected = 'a'
13201376
instance = EncryptedDiff.objects.create(
13211377
pub_field=expected,
13221378
sym_field=expected,
@@ -1325,7 +1381,7 @@ def test_write_to_diff_keys(self):
13251381
)
13261382

13271383
reset_queries() # Required for Django 1.11
1328-
instance = EncryptedDiff.objects.get(id=1)
1384+
instance = EncryptedDiff.objects.get()
13291385

13301386
self.assertTrue(
13311387
instance.pub_field,

0 commit comments

Comments
 (0)