Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

For consideration: EncryptedFileField #13

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------
Expand All @@ -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
------------
Expand All @@ -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.
9 changes: 4 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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 = '[email protected]',
url = 'http://github.com/svetlyak40wt/django-fields/',
url = 'http://github.com/bryanhelmig/django-fields/',
install_requires = ['pycrypto', ],
classifiers=[
'Development Status :: 2 - Pre-Alpha',
Expand All @@ -20,7 +21,5 @@
],
package_dir = {'': 'src'},
packages = ['django_fields'],
include_package_data = True,
#include_package_data = True,
)


91 changes: 90 additions & 1 deletion src/django_fields/fields.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import base64
import binascii
import datetime
import random
Expand All @@ -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)
Expand Down Expand Up @@ -279,14 +281,101 @@ 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([
(
[
BaseEncryptedField, EncryptedDateField, BaseEncryptedDateField, EncryptedCharField, EncryptedTextField,
EncryptedFloatField, EncryptedDateTimeField, BaseEncryptedNumberField, EncryptedIntField, EncryptedLongField,
EncryptedUSPhoneNumberField, EncryptedEmailField,
EncryptedUSPhoneNumberField, EncryptedEmailField, EncryptedFileField,
],
[],
{
Expand Down
15 changes: 14 additions & 1 deletion src/django_fields/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -52,6 +53,10 @@ class USPhoneNumberField(models.Model):
phone = EncryptedUSPhoneNumberField()


class EncFile(models.Model):
attachment = EncryptedFileField()


class EncryptTests(unittest.TestCase):

def setUp(self):
Expand Down Expand Up @@ -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