diff --git a/README.md b/README.md index f310f41..9a6ea08 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,9 @@ class MyModelManager(managers.PGPManager): 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') integer_pgp_pub_field = fields.IntegerPGPPublicKeyField() pgp_pub_field = fields.TextPGPPublicKeyField() @@ -161,6 +163,21 @@ 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 + +class User(models.Models): + 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. + #### Decryption using custom model managers If you use the bundled `PGPManager` with your custom model manager, all encrypted diff --git a/pgcrypto/mixins.py b/pgcrypto/mixins.py index 9799f35..bd301ad 100644 --- a/pgcrypto/mixins.py +++ b/pgcrypto/mixins.py @@ -20,6 +20,18 @@ class HashMixin: `HashMixin` uses 'pgcrypto' to encrypt data in a postgres database. """ + def __init__(self, original=None, *args, **kwargs): + self.original = original + + super(HashMixin, self).__init__(*args, **kwargs) + + def pre_save(self, model_instance, add): + 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. diff --git a/tests/models.py b/tests/models.py index 014b876..8c88ece 100644 --- a/tests/models.py +++ b/tests/models.py @@ -10,7 +10,9 @@ class EncryptedModelManager(managers.PGPManager): 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) integer_pgp_pub_field = fields.IntegerPGPPublicKeyField(blank=True, null=True) diff --git a/tests/test_fields.py b/tests/test_fields.py index da5a7ca..290b710 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -73,7 +73,9 @@ def test_fields(self): expected = ( 'id', 'digest_field', + 'digest_with_original_field', 'hmac_field', + 'hmac_with_original_field', 'email_pgp_pub_field', 'integer_pgp_pub_field', 'pgp_pub_field', @@ -199,6 +201,15 @@ def test_digest_lookup(self): 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' @@ -208,6 +219,15 @@ def test_hmac_lookup(self): 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)