From b3cdb794bcc8da0853acf0d6ce02ef9dd41acdd0 Mon Sep 17 00:00:00 2001 From: Bryan Helmig Date: Fri, 18 Nov 2011 11:16:52 -0600 Subject: [PATCH 01/12] add encrypted filefield, no tests yet --- src/django_fields/fields.py | 85 ++++++++++++++++++++++++++++++++++++- src/django_fields/tests.py | 15 ++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/django_fields/fields.py b/src/django_fields/fields.py index a86107e..03ef8aa 100644 --- a/src/django_fields/fields.py +++ b/src/django_fields/fields.py @@ -11,6 +11,7 @@ from django.utils.encoding import smart_str, force_unicode from django.utils.translation import ugettext_lazy as _ +from django.core.files.base import ContentFile USE_CPICKLE = getattr(settings, 'USE_CPICKLE', False) @@ -279,6 +280,88 @@ def formfield(self, **kwargs): return super(EncryptedEmailField, self).formfield(**defaults) +class EncryptedFieldFile(models.fields.files.FieldFile): + """ + TODO: + Should probably use chunks (if available) to encrypt/decrypt instead of + loading in memory. + """ + + def __init__(self, *args, **kwargs): + super(EncryptedFieldFile, self).__init__(*args, **kwargs) + + self.cipher_type = kwargs.pop('cipher', 'AES') + try: + imp = __import__('Crypto.Cipher', globals(), locals(), [self.cipher_type], -1) + except: + imp = __import__('Crypto.Cipher', globals(), locals(), [self.cipher_type]) + self.cipher = getattr(imp, self.cipher_type).new(settings.SECRET_KEY[:32]) + self.prefix = '$%s$' % self.cipher_type + + def _get_padding(self, value): + # We always want at least 2 chars of padding (including zero byte), + # so we could have up to block_size + 1 chars. + mod = (len(value) + 2) % self.cipher.block_size + return self.cipher.block_size - mod + 2 + + def encrypt(self, content): + """ + Returns an encrypted string based ContentFile. + """ + value = content.read() + + value = base64.encodestring(value) + + padding = self._get_padding(value) + if padding > 0: + value += "\0" + ''.join([random.choice(string.printable) + for index in range(padding-1)]) + value = self.prefix + binascii.b2a_hex(self.cipher.encrypt(value)) + + return ContentFile(value) + + def decrypt(self, content=None): + """ + Returns a decrypted binary based ContentFile. + """ + if not content: + self.open(mode='rb') + value = self.read() + else: + value = content.read() + + value = self.cipher.decrypt( + binascii.a2b_hex(value[len(self.prefix):]) + ).split('\0')[0] + + value = base64.decodestring(value) + + return ContentFile(value) + + def save(self, name, content, save=True): + content = self.encrypt(content) + super(EncryptedFieldFile, self).save(name, content, save) + + +class EncryptedFileField(models.FileField): + """ + Encrypts a file via `EncryptedFieldFile` before it is saved to the storage backend. + + Uploading or adding a file is the same as a normal FileField. + + Downloading is different, as `instance.attachment.url` will give you a useless link + to an encrypted file. You'll need to do something like this to stream an unencrypted + file back to the user: + + path = instance.attachment.path + response = HttpResponse(FileWrapper(instance.attachment.decrypt()), content_type=mimetypes.guess_type(path)) + response['Content-Disposition'] = 'attachment; filename=' + path.split('/')[-1] + return response + + """ + attr_class = EncryptedFieldFile + + try: from south.modelsinspector import add_introspection_rules add_introspection_rules([ @@ -286,7 +369,7 @@ def formfield(self, **kwargs): [ BaseEncryptedField, EncryptedDateField, BaseEncryptedDateField, EncryptedCharField, EncryptedTextField, EncryptedFloatField, EncryptedDateTimeField, BaseEncryptedNumberField, EncryptedIntField, EncryptedLongField, - EncryptedUSPhoneNumberField, EncryptedEmailField, + EncryptedUSPhoneNumberField, EncryptedEmailField, EncryptedFileField, ], [], { diff --git a/src/django_fields/tests.py b/src/django_fields/tests.py index aa6aabc..256db2c 100644 --- a/src/django_fields/tests.py +++ b/src/django_fields/tests.py @@ -10,7 +10,8 @@ from fields import (EncryptedCharField, EncryptedDateField, EncryptedDateTimeField, EncryptedIntField, EncryptedLongField, EncryptedFloatField, PickleField, - EncryptedUSPhoneNumberField, EncryptedEmailField) + EncryptedUSPhoneNumberField, EncryptedEmailField, + EncryptedFileField) class EncObject(models.Model): max_password = 20 @@ -52,6 +53,10 @@ class USPhoneNumberField(models.Model): phone = EncryptedUSPhoneNumberField() +class EncFile(models.Model): + attachment = EncryptedFileField() + + class EncryptTests(unittest.TestCase): def setUp(self): @@ -347,3 +352,11 @@ def _get_two_emails(self, email_length): return enc_email_1, enc_email_2 +class EncryptFileTests(unittest.TestCase): + + def setUp(self): + EncFile.objects.all().delete() + + def test_placeholder(self): + """ coming soon... """ + pass From 9cdb083c78db65dc92ff6f729eb3b73d9a00922b Mon Sep 17 00:00:00 2001 From: Bryan Helmig Date: Fri, 18 Nov 2011 11:21:28 -0600 Subject: [PATCH 02/12] adding forgotten import --- src/django_fields/fields.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/django_fields/fields.py b/src/django_fields/fields.py index 03ef8aa..22def98 100644 --- a/src/django_fields/fields.py +++ b/src/django_fields/fields.py @@ -1,3 +1,4 @@ +import base64 import binascii import datetime import random From e1a61ded00fc3ae4a6b16b78c42873cfd4c6ee80 Mon Sep 17 00:00:00 2001 From: Bryan Helmig Date: Fri, 18 Nov 2011 11:28:29 -0600 Subject: [PATCH 03/12] adding more to readme --- README.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8b61427..1c14504 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,24 @@ Introduction Django-fields is an application which includes different kinds of models fields. -Right now, application contains two fields with encryption support: -EncryptedCharField and EncryptedTextField. +Right now, the application contains these fields with encryption support: + +* EncryptedCharField +* EncryptedDateField +* EncryptedDateTimeField +* EncryptedEmailField +* EncryptedFileField +* EncryptedFloatField +* EncryptedIntField +* EncryptedLongField +* EncryptedTextField +* EncryptedUSPhoneNumberField + +They are each used in a similar fashion to their native, non-encrypted counterparts. + +One thing to remember is `.filter()`, `.order_by()`, etc... methods on a queryset will +not work due to the text being encrypted in the database. Any filtering, sorting, etc... +on encrypted fields will need to be done in memory. Requirements ----------- @@ -19,7 +35,9 @@ Under Ubuntu, just do: Examples -------- -Examples can be found at the `examples` directory. Look at the, `tests.py`. +Examples can be found at the `examples` directory. Look at `tests.py`. + +Also check out the doc strings for various special use cases (especially EncryptedFileField). Contributors ------------ From 135676dabf5cee2ba53459f518440f3514f0ab1e Mon Sep 17 00:00:00 2001 From: Bryan Helmig Date: Fri, 18 Nov 2011 11:30:27 -0600 Subject: [PATCH 04/12] spelling errors in readme, adding myself to contrib --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1c14504..ee03bb9 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Contributors his [django snippet](http://www.djangosnippets.org/snippets/1095/) for encrypted fields. After some fixes, this snippet works as supposed. * John Morrissey — for fixing bug in PickleField. -* Joe Jasinski — different fixes and new fields for encripted email and US Phone. -* Colin MacDonald — for many encripted fields added. +* Joe Jasinski — different fixes and new fields for encrypted email and US Phone. +* Colin MacDonald — for many encrypted fields added. * Igor Davydenko — PickleField. +* Bryan Helmig - encrypted file field. From cd0c560f454c8d35b4a23e2b724a0314966b7fcb Mon Sep 17 00:00:00 2001 From: Bryan Helmig Date: Fri, 18 Nov 2011 15:18:39 -0600 Subject: [PATCH 05/12] update version # --- README.md | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ee03bb9..a479a0d 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,4 @@ Contributors * Joe Jasinski — different fixes and new fields for encrypted email and US Phone. * Colin MacDonald — for many encrypted fields added. * Igor Davydenko — PickleField. -* Bryan Helmig - encrypted file field. +* Bryan Helmig — encrypted file field. diff --git a/setup.py b/setup.py index 0e2383e..bdbbd02 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,13 @@ from setuptools import setup, find_packages setup( name = 'django-fields', - version = '0.1.3', + version = '0.1.4', description = 'Django-fields is an application which includes different kinds of models fields.', keywords = 'django apps tools collection', license = 'New BSD License', author = 'Alexander Artemenko', author_email = 'svetlyak.40wt@gmail.com', - url = 'http://github.com/svetlyak40wt/django-fields/', + url = 'http://github.com/bryanhelmig/django-fields/', install_requires = ['pycrypto', ], classifiers=[ 'Development Status :: 2 - Pre-Alpha', From 47bf5a42db3536cc9fed8a1bcd89454493ac65d1 Mon Sep 17 00:00:00 2001 From: Bryan Date: Tue, 6 Dec 2011 15:12:49 -0600 Subject: [PATCH 06/12] remove package data --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bdbbd02..f61cd7e 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ ], package_dir = {'': 'src'}, packages = ['django_fields'], - include_package_data = True, + #include_package_data = True, ) From acd5d5ec9f9353ca2b7503f7852bb186db9dc52c Mon Sep 17 00:00:00 2001 From: Bryan Date: Tue, 6 Dec 2011 15:17:14 -0600 Subject: [PATCH 07/12] Update setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index f61cd7e..df05646 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ author = 'Alexander Artemenko', author_email = 'svetlyak.40wt@gmail.com', url = 'http://github.com/bryanhelmig/django-fields/', + download_url='https://github.com/bryanhelmig/django-fields/', install_requires = ['pycrypto', ], classifiers=[ 'Development Status :: 2 - Pre-Alpha', From 4a6d257bbe23dad1d623999d7ca1371f7de0827c Mon Sep 17 00:00:00 2001 From: Bryan Date: Tue, 6 Dec 2011 15:20:20 -0600 Subject: [PATCH 08/12] Update setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index df05646..7735a23 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ 'Topic :: Software Development :: Libraries :: Python Modules', ], package_dir = {'': 'src'}, + zip_safe = True, packages = ['django_fields'], #include_package_data = True, ) From ab8856773a299230e0f8c5cb8cb702f939d98eee Mon Sep 17 00:00:00 2001 From: Bryan Date: Tue, 6 Dec 2011 15:21:23 -0600 Subject: [PATCH 09/12] Update setup.py --- setup.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/setup.py b/setup.py index 7735a23..4d02b73 100644 --- a/setup.py +++ b/setup.py @@ -1,26 +1,10 @@ from setuptools import setup, find_packages + setup( - name = 'django-fields', - version = '0.1.4', - description = 'Django-fields is an application which includes different kinds of models fields.', - keywords = 'django apps tools collection', - license = 'New BSD License', - author = 'Alexander Artemenko', - author_email = 'svetlyak.40wt@gmail.com', url = 'http://github.com/bryanhelmig/django-fields/', download_url='https://github.com/bryanhelmig/django-fields/', install_requires = ['pycrypto', ], - classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'Environment :: Plugins', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: Python', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], package_dir = {'': 'src'}, - zip_safe = True, packages = ['django_fields'], #include_package_data = True, ) From 3d9e1e4b5c950b27117373de92e5dea5e8ac78b9 Mon Sep 17 00:00:00 2001 From: Bryan Date: Tue, 6 Dec 2011 15:22:51 -0600 Subject: [PATCH 10/12] Update setup.py --- setup.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 4d02b73..441ba3b 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,25 @@ from setuptools import setup, find_packages setup( + name = 'django-fields', + version = '0.1.4', + description = 'Django-fields is an application which includes different kinds of models fields.', + keywords = 'django apps tools collection', + license = 'New BSD License', + author = 'Alexander Artemenko', + author_email = 'svetlyak.40wt@gmail.com', url = 'http://github.com/bryanhelmig/django-fields/', - download_url='https://github.com/bryanhelmig/django-fields/', install_requires = ['pycrypto', ], + classifiers=[ + 'Development Status :: 2 - Pre-Alpha', + 'Environment :: Plugins', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], package_dir = {'': 'src'}, packages = ['django_fields'], #include_package_data = True, ) - - From 5e51aae9f6c551824c75d8279fb77ec316082b59 Mon Sep 17 00:00:00 2001 From: Bryan Helmig Date: Mon, 6 Feb 2012 14:19:55 -0600 Subject: [PATCH 11/12] fix south freak out, hack fix --- src/django_fields/fields.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/django_fields/fields.py b/src/django_fields/fields.py index 22def98..19592a8 100644 --- a/src/django_fields/fields.py +++ b/src/django_fields/fields.py @@ -360,6 +360,11 @@ class EncryptedFileField(models.FileField): return response """ + def __init__(self, *args, **kwargs): + super(EncryptedFileField, self).__init__(*args, **kwargs) + # this is ignored, and is hack + self.cipher_type = kwargs.pop('cipher', 'AES') + attr_class = EncryptedFieldFile From ebbbc7574f8835b4d4b4ad05d1db0d40ef122085 Mon Sep 17 00:00:00 2001 From: Bryan Helmig Date: Mon, 6 Feb 2012 14:20:56 -0600 Subject: [PATCH 12/12] tiny fix --- src/django_fields/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/django_fields/fields.py b/src/django_fields/fields.py index 19592a8..c329e82 100644 --- a/src/django_fields/fields.py +++ b/src/django_fields/fields.py @@ -361,9 +361,9 @@ class EncryptedFileField(models.FileField): """ def __init__(self, *args, **kwargs): - super(EncryptedFileField, self).__init__(*args, **kwargs) # this is ignored, and is hack self.cipher_type = kwargs.pop('cipher', 'AES') + super(EncryptedFileField, self).__init__(*args, **kwargs) attr_class = EncryptedFieldFile