diff --git a/src/django_fields/fields.py b/src/django_fields/fields.py index 0075a6f..0cc0c77 100644 --- a/src/django_fields/fields.py +++ b/src/django_fields/fields.py @@ -1,4 +1,5 @@ import binascii +import codecs import datetime import string import sys @@ -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 @@ -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/''' @@ -76,7 +84,12 @@ 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), @@ -84,7 +97,7 @@ def _get_padding(self, value): 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)] @@ -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 @@ -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): @@ -136,8 +160,6 @@ def deconstruct(self): class EncryptedTextField(BaseEncryptedField): - __metaclass__ = models.SubfieldBase - def get_internal_type(self): return 'TextField' @@ -148,8 +170,6 @@ def formfield(self, **kwargs): class EncryptedCharField(BaseEncryptedField): - __metaclass__ = models.SubfieldBase - def get_internal_type(self): return "CharField" @@ -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: @@ -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 @@ -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" @@ -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" @@ -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 @@ -267,16 +287,20 @@ 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): @@ -284,7 +308,6 @@ def get_internal_type(self): 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. @@ -292,22 +315,37 @@ class EncryptedFloatField(BaseEncryptedNumberField): 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: @@ -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 @@ -334,8 +370,6 @@ def formfield(self, **kwargs): class EncryptedUSSocialSecurityNumberField(BaseEncryptedField): - __metaclass__ = models.SubfieldBase - def get_internal_type(self): return "CharField" @@ -343,14 +377,13 @@ 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): diff --git a/src/django_fields/models.py b/src/django_fields/models.py index b1afa50..3b7e3ed 100644 --- a/src/django_fields/models.py +++ b/src/django_fields/models.py @@ -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 @@ -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 diff --git a/src/django_fields/tests.py b/src/django_fields/tests.py index 971c994..33ce283 100644 --- a/src/django_fields/tests.py +++ b/src/django_fields/tests.py @@ -20,6 +20,11 @@ from .models import ModelWithPrivateFields +if sys.version_info[0] == 3: + PYTHON3 = True +else: + PYTHON3 = False + class EncObject(models.Model): max_password = 20 @@ -180,14 +185,14 @@ def test_none_value(self): def _get_encrypted_password(self, id): cursor = connection.cursor() cursor.execute("select password from django_fields_encobject where id = %s", [id,]) - passwords = map(lambda x: x[0], cursor.fetchall()) + passwords = list(map(lambda x: x[0], cursor.fetchall())) self.assertEqual(len(passwords), 1) # only one return passwords[0] def _get_encrypted_password_cipher(self, id): cursor = connection.cursor() cursor.execute("select password from django_fields_cipherencobject where id = %s", [id,]) - passwords = map(lambda x: x[0], cursor.fetchall()) + passwords = list(map(lambda x: x[0], cursor.fetchall())) self.assertEqual(len(passwords), 1) # only one return passwords[0] @@ -254,21 +259,21 @@ def test_date_encryption_w_cipher(self): def _get_encrypted_date(self, id): cursor = connection.cursor() cursor.execute("select important_date from django_fields_encdate where id = %s", [id,]) - important_dates = map(lambda x: x[0], cursor.fetchall()) + important_dates = list(map(lambda x: x[0], cursor.fetchall())) self.assertEqual(len(important_dates), 1) # only one return important_dates[0] def _get_encrypted_datetime(self, id): cursor = connection.cursor() cursor.execute("select important_datetime from django_fields_encdatetime where id = %s", [id,]) - important_datetimes = map(lambda x: x[0], cursor.fetchall()) + important_datetimes = list(map(lambda x: x[0], cursor.fetchall())) self.assertEqual(len(important_datetimes), 1) # only one return important_datetimes[0] def _get_encrypted_date_cipher(self, id): cursor = connection.cursor() cursor.execute("select important_date from django_fields_cipherencdate where id = %s", [id,]) - important_dates = map(lambda x: x[0], cursor.fetchall()) + important_dates = list(map(lambda x: x[0], cursor.fetchall())) self.assertEqual(len(important_dates), 1) # only one return important_dates[0] @@ -280,20 +285,37 @@ def setUp(self): EncFloat.objects.all().delete() def test_int_encryption(self): - self._test_number_encryption(EncInt, 'int', sys.maxint) + if PYTHON3 is True: + self._test_number_encryption(EncInt, 'int', sys.maxsize) + else: + self._test_number_encryption(EncInt, 'int', sys.maxint) def test_min_int_encryption(self): - self._test_number_encryption(EncInt, 'int', -sys.maxint - 1) + if PYTHON3 is True: + self._test_number_encryption(EncInt, 'int', -sys.maxsize - 1) + else: + self._test_number_encryption(EncInt, 'int', -sys.maxint - 1) def test_long_encryption(self): - self._test_number_encryption(EncLong, 'long', long(sys.maxint) * 100L) + if PYTHON3 is True: + self._test_number_encryption( + EncLong, 'long', int(sys.maxsize) * 100) + else: + self._test_number_encryption( + EncLong, 'long', long(sys.maxint) * long(100)) def test_float_encryption(self): - value = 123.456 + sys.maxint + if PYTHON3 is True: + value = 123.456 + sys.maxsize + else: + value = 123.456 + sys.maxint self._test_number_encryption(EncFloat, 'float', value) def test_one_third_float_encryption(self): - value = sys.maxint + (1.0 / 3.0) + if PYTHON3 is True: + value = sys.maxsize + (1.0 / 3.0) + else: + value = sys.maxint + (1.0 / 3.0) self._test_number_encryption(EncFloat, 'float', value) def _test_number_encryption(self, number_class, type_name, value): @@ -311,7 +333,7 @@ def _get_encrypted_number(self, type_name, id): cursor = connection.cursor() sql = "select important_number from django_fields_enc%s where id = %%s" % (type_name,) cursor.execute(sql, [id,]) - important_numbers = map(lambda x: x[0], cursor.fetchall()) + important_numbers = list(map(lambda x: x[0], cursor.fetchall())) self.assertEqual(len(important_numbers), 1) # only one return important_numbers[0] @@ -418,7 +440,7 @@ def test_minimum_padding(self): def _get_encrypted_email(self, id): cursor = connection.cursor() cursor.execute("select email from django_fields_emailobject where id = %s", [id,]) - emails = map(lambda x: x[0], cursor.fetchall()) + emails = list(map(lambda x: x[0], cursor.fetchall())) self.assertEqual(len(emails), 1) # only one return emails[0] @@ -490,7 +512,7 @@ def test_cipher_storage_length_versus_schema_length(self): def _get_raw_password_value(self, id): cursor = connection.cursor() cursor.execute("select password from django_fields_cipherencobject where id = %s", [id, ]) - passwords = map(lambda x: x[0], cursor.fetchall()) + passwords = list(map(lambda x: x[0], cursor.fetchall())) self.assertEqual(len(passwords), 1) # only one return passwords[0]