diff --git a/README.md b/README.md index 8b61427..a479a0d 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 ------------ @@ -28,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. diff --git a/setup.py b/setup.py index 0e2383e..441ba3b 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,14 @@ 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', @@ -20,7 +21,5 @@ ], package_dir = {'': 'src'}, packages = ['django_fields'], - include_package_data = True, + #include_package_data = True, ) - - diff --git a/src/django_fields/fields.py b/src/django_fields/fields.py index a86107e..c329e82 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 @@ -11,6 +12,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 +281,93 @@ 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 + + """ + def __init__(self, *args, **kwargs): + # this is ignored, and is hack + self.cipher_type = kwargs.pop('cipher', 'AES') + super(EncryptedFileField, self).__init__(*args, **kwargs) + + attr_class = EncryptedFieldFile + + try: from south.modelsinspector import add_introspection_rules add_introspection_rules([ @@ -286,7 +375,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