Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 67 additions & 34 deletions src/django_fields/fields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import binascii
import codecs
import datetime
import string
import sys
Expand All @@ -8,7 +9,6 @@
from django.forms import fields
from django.db import models
from django.conf import settings
from django.utils.encoding import smart_str, force_unicode
from django.utils.translation import ugettext_lazy as _
from Crypto import Random
from Crypto.Random import random
Expand All @@ -28,6 +28,14 @@
except:
import pickle

if sys.version_info[0] == 3:
PYTHON3 = True
from django.utils.encoding import smart_str, force_text as force_unicode
else:
PYTHON3 = False
from django.utils.encoding import smart_str, force_unicode


class BaseEncryptedField(models.Field):
'''This code is based on the djangosnippet #1095
You can find the original at http://www.djangosnippets.org/snippets/1095/'''
Expand Down Expand Up @@ -76,15 +84,20 @@ def __init__(self, *args, **kwargs):
super(BaseEncryptedField, self).__init__(*args, **kwargs)

def _is_encrypted(self, value):
return isinstance(value, basestring) and value.startswith(self.prefix)
if PYTHON3 is True:
is_enc = isinstance(value, str) and value.startswith(self.prefix)
return is_enc
else:
return isinstance(value, basestring) and value.startswith(
self.prefix)

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 to_python(self, value):
def from_db_value(self, value, expression, connection, context):
if self._is_encrypted(value):
if self.block_type:
self.iv = binascii.a2b_hex(value[len(self.prefix):])[:len(self.iv)]
Expand All @@ -96,7 +109,7 @@ def to_python(self, value):
else:
decrypt_value = binascii.a2b_hex(value[len(self.prefix):])
return force_unicode(
self.cipher.decrypt(decrypt_value).split('\0')[0]
self.cipher.decrypt(decrypt_value).split(b'\0')[0]
)
return value

Expand All @@ -118,9 +131,20 @@ def get_db_prep_value(self, value, connection=None, prepared=False):
self.secret_key,
getattr(self.cipher_object, self.block_type),
self.iv)
value = self.prefix + binascii.b2a_hex(self.iv + self.cipher.encrypt(value))
if PYTHON3 is True:
value = self.prefix + binascii.b2a_hex(
self.iv + self.cipher.encrypt(value)).decode('utf-8')
else:
value = self.prefix + binascii.b2a_hex(
self.iv + self.cipher.encrypt(value))
else:
value = self.prefix + binascii.b2a_hex(self.cipher.encrypt(value))
if PYTHON3 is True:
print('>>>', value, '-->', len(value)/16)
value = self.prefix + binascii.b2a_hex(
self.cipher.encrypt(value)).decode('utf-8')
else:
value = self.prefix + binascii.b2a_hex(
self.cipher.encrypt(value))
return value

def deconstruct(self):
Expand All @@ -136,8 +160,6 @@ def deconstruct(self):


class EncryptedTextField(BaseEncryptedField):
__metaclass__ = models.SubfieldBase

def get_internal_type(self):
return 'TextField'

Expand All @@ -148,8 +170,6 @@ def formfield(self, **kwargs):


class EncryptedCharField(BaseEncryptedField):
__metaclass__ = models.SubfieldBase

def get_internal_type(self):
return "CharField"

Expand Down Expand Up @@ -190,7 +210,7 @@ def formfield(self, **kwargs):
defaults.update(kwargs)
return super(BaseEncryptedDateField, self).formfield(**defaults)

def to_python(self, value):
def from_db_value(self, value, expression, connection, context):
# value is either a date or a string in the format "YYYY:MM:DD"

if value in fields.EMPTY_VALUES:
Expand All @@ -199,7 +219,8 @@ def to_python(self, value):
if isinstance(value, self.date_class):
date_value = value
else:
date_text = super(BaseEncryptedDateField, self).to_python(value)
date_text = super(BaseEncryptedDateField, self).from_db_value(
value, expression, connection, context)
date_value = self.date_class(*map(int, date_text.split(':')))
return date_value

Expand All @@ -218,7 +239,6 @@ def get_db_prep_value(self, value, connection=None, prepared=False):


class EncryptedDateField(BaseEncryptedDateField):
__metaclass__ = models.SubfieldBase
form_widget = forms.DateInput
form_field = forms.DateField
save_format = "%Y:%m:%d"
Expand All @@ -228,7 +248,6 @@ class EncryptedDateField(BaseEncryptedDateField):

class EncryptedDateTimeField(BaseEncryptedDateField):
# FIXME: This doesn't handle time zones, but Python doesn't really either.
__metaclass__ = models.SubfieldBase
form_widget = forms.DateTimeInput
form_field = forms.DateTimeField
save_format = "%Y:%m:%d:%H:%M:%S:%f"
Expand All @@ -247,12 +266,13 @@ def __init__(self, *args, **kwargs):
def get_internal_type(self):
return 'CharField'

def to_python(self, value):
def from_db_value(self, value, expression, connection, context):
# value is either an int or a string of an integer
if isinstance(value, self.number_type) or value == '':
number = value
else:
number_text = super(BaseEncryptedNumberField, self).to_python(value)
number_text = super(BaseEncryptedNumberField, self).from_db_value(
value, expression, connection, context)
number = self.number_type(number_text)
return number

Expand All @@ -267,47 +287,65 @@ def get_db_prep_value(self, value, connection=None, prepared=False):


class EncryptedIntField(BaseEncryptedNumberField):
__metaclass__ = models.SubfieldBase
max_raw_length = len(str(-sys.maxint - 1))
if PYTHON3 is True:
max_raw_length = len(str(-sys.maxsize - 1))
else:
max_raw_length = len(str(-sys.maxint - 1))
number_type = int
format_string = "%d"


class EncryptedLongField(BaseEncryptedNumberField):
__metaclass__ = models.SubfieldBase
max_raw_length = None # no limit
number_type = long
if PYTHON3 is True:
number_type = int
else:
number_type = long
format_string = "%d"

def get_internal_type(self):
return 'TextField'


class EncryptedFloatField(BaseEncryptedNumberField):
__metaclass__ = models.SubfieldBase
max_raw_length = 150 # arbitrary, but should be sufficient
number_type = float
# If this format is too long for some architectures, change it.
format_string = "%0.66f"


class PickleField(models.TextField):
__metaclass__ = models.SubfieldBase

editable = False
serialize = False

def get_db_prep_value(self, value, connection=None, prepared=False):
return pickle.dumps(value)
if PYTHON3 is True:
# When PYTHON3, we convert data to base64 to prevent errors when
# unpickling.
val = codecs.encode(pickle.dumps(value), 'base64').decode()
return val
else:
return pickle.dumps(value)

def to_python(self, value):
if not isinstance(value, basestring):
return value
def from_db_value(self, value, expression, connection, context):
if PYTHON3 is True:
if not isinstance(value, str):
return value
else:
if not isinstance(value, basestring):
return value

# Tries to convert unicode objects to string, cause loads pickle from
# unicode excepts ugly ``KeyError: '\x00'``.
try:
return pickle.loads(smart_str(value))
if PYTHON3 is True:
# When PYTHON3, data are in base64 to prevent errors when
# unpickling.
val = pickle.loads(codecs.decode(value.encode(), "base64"))
return val
else:
return pickle.loads(smart_str(value))
# If pickle could not loads from string it's means that it's Python
# string saved to PickleField.
except ValueError:
Expand All @@ -317,14 +355,12 @@ def to_python(self, value):


class EncryptedUSPhoneNumberField(BaseEncryptedField):
__metaclass__ = models.SubfieldBase

def get_internal_type(self):
return "CharField"

def formfield(self, **kwargs):
try:
from localflavor.us.forms import USPhoneNumberField
from localflavor.us.forms import USPhoneNumberField
except ImportError:
from django.contrib.localflavor.us.forms import USPhoneNumberField

Expand All @@ -334,23 +370,20 @@ def formfield(self, **kwargs):


class EncryptedUSSocialSecurityNumberField(BaseEncryptedField):
__metaclass__ = models.SubfieldBase

def get_internal_type(self):
return "CharField"

def formfield(self, **kwargs):
try:
from localflavor.us.forms import USSocialSecurityNumberField
except ImportError:
from django.contrib.localflavor.us.forms import USSocialSecurityNumberField
from django.contrib.localflavor.us.forms import USSocialSecurityNumberField

defaults = {'form_class': USSocialSecurityNumberField}
defaults.update(kwargs)
return super(EncryptedUSSocialSecurityNumberField, self).formfield(**defaults)

class EncryptedEmailField(BaseEncryptedField):
__metaclass__ = models.SubfieldBase
description = _("E-mail address")

def get_internal_type(self):
Expand Down
12 changes: 11 additions & 1 deletion src/django_fields/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# -*- coding: utf-8 -*-

import re
import sys
from django.db import models

if sys.version_info[0] == 3:
PYTHON3 = True
else:
PYTHON3 = False


class PrivateFieldsMetaclass(models.base.ModelBase):
"""Metaclass to set right default db_column values
Expand Down Expand Up @@ -64,7 +70,11 @@ def __init__(self, *args, **kwargs):

field_names = set(f.name for f in self._meta.fields)

for key, value in kwargs.iteritems():
if PYTHON3 is True:
items = kwargs.items()
else:
items = kwargs.iteritems()
for key, value in items:
if prefix + key in field_names:
key = prefix + key
new_kwargs[key] = value
Expand Down
Loading