Skip to content

Commit

Permalink
Finished release 0.1.7.
Browse files Browse the repository at this point in the history
  • Loading branch information
erikvw committed Jul 5, 2016
2 parents 92643c4 + 8c59cae commit e885432
Show file tree
Hide file tree
Showing 21 changed files with 274 additions and 42 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Expand Up @@ -13,7 +13,7 @@ south.log
htmlcov
.coverage
db.sqlite3
*.key
*.pem
# *.key
# *.pem
build
django_crypto_fields.egg-info
10 changes: 6 additions & 4 deletions .travis.yml
Expand Up @@ -4,16 +4,18 @@ python:
# - "3.2"
# - "3.3"
- "3.4"
- "3.5"
# - "nightly"
env:
- DJANGO_VERSION=1.7
- DJANGO_VERSION=1.8
- DJANGO_VERSION=1.9

install:
- pip install -q Django==$DJANGO_VERSION --use-mirrors
- pip install flake8 --use-mirrors
- pip install coveralls --use-mirrors
- pip install -q -r requirements.txt --use-mirrors
- pip install -q Django==$DJANGO_VERSION
- pip install flake8
- pip install coveralls
- pip install -q -r requirements.txt

before_script:
- flake8 django_crypto_fields
Expand Down
28 changes: 18 additions & 10 deletions README.md
Expand Up @@ -29,13 +29,17 @@ For example:
Installation
------------

pip install django-encrypted-fields
pip install git+https://github.com/erikvw/django-crypto-fields@develop#egg=django-crypto-fields

Add to INSTALLED_APPS:
Macosx installation issue with `pycrypto`. `django-crypto-fields` requires `pycrypto`. If `pycrypto` has trouble installing try:

CFLAGS="-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk -I/usr/local/include" LDFLAGS="-L/usr/local/lib" pip install pycrypto

Once installed, add to INSTALLED_APPS:

INSTALLED_APPS = (
...
'django_crypto_fields',
'django_crypto_fields.apps.DjangoCryptoFieldsAppConfig',
...
)

Expand All @@ -51,11 +55,20 @@ Add KEY_PREFIX (optional, the default is "_user_"):

Run _migrate_ to create the _crypto_fields_crypt_ table:

python manage.py migrate
python manage.py migrate django_crypto_fields

Generate encryption keys:

python manage.py generate_keys
python manage.py generate_keys


Legacy Mode
-----------

As of version 0.1.7, the default AES encryption mode is CBC. To run in legacy mode (CFB) add to `settings.py`:

AES_ENCRYPTION_MODE = AES.MODE_CFB


History
-------
Expand Down Expand Up @@ -87,11 +100,6 @@ Disadvantages

Other encrypted field modules are available if you just want to use encrypted field classes in Django models and do not need unique constraints nor plan to join tables on encrypted fields for analysis.

### Installation Issues
`django-crypto-fields` requires `pycrypto`. If `pycrypto` has trouble installing try:

CFLAGS="-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk -I/usr/local/include" LDFLAGS="-L/usr/local/lib" pip install pycrypto

Contribute
----------

Expand Down
2 changes: 2 additions & 0 deletions crypto_fields/user-aes-local.key
@@ -0,0 +1,2 @@
[8�\�B����G�`�D�<B�T�!nM���W������O��B� ����3͛�{�
�F�HT3ՠY�Pߍ�X�I pz>&��&��L�߲r���zl@E���!�{`�Ŕ֩�R�l+K"+P��e�-���x��&�����>(�R��-O�f}ηA|oYP��Lu:zs�l(F��ҥ�_���-�*��P�Ϋ����e{I�c��O�C� ɴ�>}���R.�~����'�S��Q�pf�
Expand Down
Binary file added crypto_fields/user-aes-restricted.key
Binary file not shown.
27 changes: 27 additions & 0 deletions crypto_fields/user-rsa-local-private.pem
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEArYR0xZuIhXa7ah1wKlMNXxqaQziSRUYl4xDlucEJ5znpvLo0
Jq84J8YXkumC0fOrtqiuXUmRPhlT+N5e8V110xQmdmBZktuFJcUriqt8ECMnhbkv
WnaKtlRz0C/j2YLXTcuOzZP/rsAYr+dJbhNC35rAFeqyu1boWjXyurfnHJGNVuRY
rdF4WlZTwr/LepGlXoLnGv2HleclJ99ImadW+L19oyL8aXpCEY5V4QncUnDTYXn3
sEC19X+4DcRYU4GC5hWq9b0aLygDq9/XJqwvIJtiO3707TtCKCw+HHPLUJewSLDo
yKrEIU3s/ZYWZMYxaelCOpB9NS76bS3b9cMOIQIDAQABAoIBAGOnagU/sWWtbi+g
npXAQzK2rZgjumYMxp+lSbqoMk7ZRPN+IYuCIIc/TrSyZc682oa6VAIByWfX1hFd
bTvECHJk5T4hUfB1IQAzQDGREVyXRBCZ7adP45SZ5jXP+1Ulvw/P88D/2d18N/F2
rQaKC2MulllABMxxOlrtFu3DxvjxcRKumLaI2PYlTIb0fTxcE7uMVdbhr8WzlUYO
2KQrltnHeZ9SRhWnS75KK5U0HYcMuCIhCXjHEgeXuAY54lWKq9sDrsuzTWnHPZet
N6gSC3Xb2FyLWeSkkrsmiBnsUmLUTl2MPRE59ZBKGEMRaJbkB1bOzzY7c/tyHJZ8
hcwBN0ECgYEAz1yOzfsl7XvkuHnbCAi/G3RyTGTNEhnIeoG4riAXrOstrLSiLajP
BjM0uDcQfH4XMj9uP1XkLdg7Wm3HfMnFSoOT/qv9/ZloXSWuPCI5eWTiujv/FnNZ
Ea5TDNsqIkz23hqCXJ9kVpi59wyBoVwy789OsPnrhOyDCeNozjIojEkCgYEA1jeo
h8yzxpC6g3wWisFpMnh1rZrKGXAnUzSEnzqctZXy/EWcMAHTAY5Bt2epd0JnHY9S
2H25r40sUYFQU/muLCvoV572GAcFvmeYZoWTZOKRs+sY2emRLLd2BMb3rGE+27sK
7JGWv2hh2AVgsLGfByx5PXjOap32FQDlgGfkgxkCgYBFIlzSO/unM/lLsATgptng
c3BwG+NrN4FpprpGA5khI7+0pvyzMGblcEkRafprJzbgn9lH8IEE7+TJ/3PVHQhU
EY5UCE/EL1NmStvOXLLA0GvvH3WqrIZzqKUz0hY4HWTpI/l2nAW+AWlu/pJxk+W/
omRgCNMHHDuMHm+g34Q5UQKBgBULJq0geNCayxe7iuNamKDyoGzLdesFz3cnA5g4
w8g60MHjfLJim33doasIrecfSM3olHTb9O+/6IihMWjucCO6tHpXDUnS9sd7mccI
fDA+6Z1JDQnrHe3Mn/VcLlxquSgQucP/kVWARYxZCuSQrlSgE9DsGyryFYh+rAzv
J82RAoGAYFolTXqEYUODDOUxdWxfdt5IeFRpE1O1kIJCipURwJ+OlhC/h4JPOXfV
IGIHVxRv4JR1C0VThSEfEgcy6OWxQ6Ypl7QcGud1ZRRLQHlIck8y6c67a7g69vwt
Hyrv/LT/L7cI2G3V3a3A8/43BoXqXjSWeRX9xXEW5WaYwHA6tkM=
-----END RSA PRIVATE KEY-----
9 changes: 9 additions & 0 deletions crypto_fields/user-rsa-local-public.pem
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArYR0xZuIhXa7ah1wKlMN
XxqaQziSRUYl4xDlucEJ5znpvLo0Jq84J8YXkumC0fOrtqiuXUmRPhlT+N5e8V11
0xQmdmBZktuFJcUriqt8ECMnhbkvWnaKtlRz0C/j2YLXTcuOzZP/rsAYr+dJbhNC
35rAFeqyu1boWjXyurfnHJGNVuRYrdF4WlZTwr/LepGlXoLnGv2HleclJ99ImadW
+L19oyL8aXpCEY5V4QncUnDTYXn3sEC19X+4DcRYU4GC5hWq9b0aLygDq9/XJqwv
IJtiO3707TtCKCw+HHPLUJewSLDoyKrEIU3s/ZYWZMYxaelCOpB9NS76bS3b9cMO
IQIDAQAB
-----END PUBLIC KEY-----
27 changes: 27 additions & 0 deletions crypto_fields/user-rsa-restricted-private.pem
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAmY6VUcutn4Pqz87jyRYLsVfgj42h9IJvdBkVzBW6A4zS5l/Q
IxyJpE/HkJ2f1z3ron7V7kCoXpbHeZ0CrpfJShCr/Wfq+Cb9vtGEgqDIETaGqFCm
wclPbhePce9jlQYKv08jZaY43yJJYXTknE9dYgfmi010soXajX0dnmu0M0uvgPeD
KGPhfho1m6IQ98K6+YHq4zDAPokRIMOTGbzklTJsCzwbe9vMHBj/7EnIb9YWXl7W
ZGXUVUFulxBcHSfGcIRKSuk538SyLfEj6tHCKpmHumLpIuSCaz4g9uTAsblHFJsL
yjAdHYi6lxtG9NmbMQQqeJT2k2RHF3z24GTfowIDAQABAoIBAFirF5fyx5/rmaRD
cA7GVxwhhT7/Zrx8sQTWE1sUVKQGJiX2v4X2zZf+pVrhWdH9JFphaQC5fz+XSznz
KwBMD2RgOiAKaIjAYxdTfnO7ias+1ATPBWuy5dXJbwA4XkVIBt6vQN5KXF6kOsE2
sWd9AIxByaM4p6EKX8gBrh+zS4uuTEchTimrury4xszHpoWJrSS9LeopHGigAo8z
ZgOG1im2xIp/mw8IC7owvT1xPLVD5ff0wliDbKUNP3xCNqgexRvvmZreoYrXOu8k
4AhsBP1l3JwU17AXdX9Y+ZEkpHFf0gJGIWN0F6L+cxPr5ehOQAk9aAnEi/bs9xOc
LNf34LkCgYEAvbBaUSBz5k2ZGmwkMxEf6W2pQIv8MuBnFO/0tmJXjI4PxcTty2D5
oUIJ2kS6XY8wnRmbEEBXGjN0QgWjL3W6UFhhxQ8wGkxeuLIK4pqs/W2wV9vMErPz
VFojoYH3IZ/0GLCNzx38+FJwwCZRJn31xFhpLw04+hZRGQBsvojqQ60CgYEAzzy2
Ip74CQpr/WBGu1Fquh+ukghOrkX/+i4aOHKNNfvU9EOczsqt7K6IBjQrgdxjITuL
0gLe+uiQtqk9la6RfTFJzoNamlcLXlL+eM15fOuKpiODOmIz+LUbHTKGCquJ92Yx
+NvCgKibLlE366g2BbrFCXSDz/rVNuvH0D5/mo8CgYBZeVLQy47/xgB+zx2wHNbB
o6FfOeiHEwRsgtcaJH8JZIUaE07bTO3S27SMCTkjIoSaxdY3rzGmwTiIX+JeY5iz
h0zux8A312gilJC/3JfWnqO8fYfgFpOjaTU/XG92o91evADX/lwxwak/aZODpYyE
xfMvZ3xFsrskIOgqY6ikrQKBgQCH4IyjIowKkXTAytFVnEpYAtHZGe1MwsTVakjy
3djMnnPAPFyiEa3702uIdchY+wFcFsjUR+GfrcDEoy6n56+o4Bo+d01+iLgBfnUv
C7ouN9nOmNY//4eH+Gbwu+G7OUHann0BaEWdSe+FbOc3cjEJjK4IRwbt5M7LlciL
N48+7QKBgQCoGbrmIGqxjWsOBtn+PzpTzeo6unJDNKFYoFkGUD2P/+5WsxOYrnxf
3qLVXcoyXnUrDzIkAyh46yZzWsLYno0glrWNE6A++lAxRBYc7vXLmkrQ32i2yEUo
OABkrFPmOCL+YkYIUNZniRZLvWHAlLxoPH5dpAPgP6naidtzYrSI4w==
-----END RSA PRIVATE KEY-----
9 changes: 9 additions & 0 deletions crypto_fields/user-rsa-restricted-public.pem
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmY6VUcutn4Pqz87jyRYL
sVfgj42h9IJvdBkVzBW6A4zS5l/QIxyJpE/HkJ2f1z3ron7V7kCoXpbHeZ0CrpfJ
ShCr/Wfq+Cb9vtGEgqDIETaGqFCmwclPbhePce9jlQYKv08jZaY43yJJYXTknE9d
Ygfmi010soXajX0dnmu0M0uvgPeDKGPhfho1m6IQ98K6+YHq4zDAPokRIMOTGbzk
lTJsCzwbe9vMHBj/7EnIb9YWXl7WZGXUVUFulxBcHSfGcIRKSuk538SyLfEj6tHC
KpmHumLpIuSCaz4g9uTAsblHFJsLyjAdHYi6lxtG9NmbMQQqeJT2k2RHF3z24GTf
owIDAQAB
-----END PUBLIC KEY-----
Binary file added crypto_fields/user-salt-local.key
Binary file not shown.
Binary file added crypto_fields/user-salt-restricted.key
Binary file not shown.
2 changes: 1 addition & 1 deletion django_crypto_fields/admin.py
Expand Up @@ -24,7 +24,7 @@ class CryptoFieldsAdminSite(AdminSite):


@admin.register(Crypt, site=crypto_fields_admin)
class CryptAdmin(admin.ModelAdmin):
class CryptModelAdmin(admin.ModelAdmin):

date_hierarchy = 'modified'

Expand Down
14 changes: 12 additions & 2 deletions django_crypto_fields/apps.py
@@ -1,13 +1,16 @@
import sys

from Crypto.Cipher import AES
from django.apps import AppConfig

from django.core.management.color import color_style
from django_crypto_fields.cryptor import Cryptor


class DjangoCryptoFieldsError(Exception):
pass

style = color_style()


class DjangoCryptoFieldsAppConfig(AppConfig):
name = 'django_crypto_fields'
Expand All @@ -23,7 +26,6 @@ def __init__(self, app_label, model_name):
from django_crypto_fields.keys import Keys
keys = Keys()
if not self.encryption_keys:
style = color_style()
sys.stdout.write('Loading {} ...\n'.format(self.verbose_name))
if not keys.key_files_exist():
sys.stdout.write(style.NOTICE('Warning: {} failed to load encryption keys.\n'.format(
Expand All @@ -37,6 +39,14 @@ def __init__(self, app_label, model_name):
keys.load_keys()
self.encryption_keys = keys

def ready(self):
cryptor = Cryptor()
if cryptor.aes_encryption_mode == AES.MODE_CFB:
sys.stdout.write(style.NOTICE(
'Warning: Encryption mode MODE_CFB should not be used. \n'
' See django_crypto_fields.cryptor.py and comments \n'
' in pycrypto.blockalgo.py.\n'))


class TestDjangoCryptoFieldsApp(DjangoCryptoFieldsAppConfig):
name = 'django_crypto_fields'
Expand Down
4 changes: 4 additions & 0 deletions django_crypto_fields/crypt_model_mixin.py
Expand Up @@ -30,6 +30,10 @@ class CryptModelMixin(models.Model):
db_index=True,
null=True)

cipher_mode = models.IntegerField(
null=True,
help_text='pycrypto AES cipher mode (e.g. MODE_CBC)')

objects = CryptModelManager()

def natural_key(self):
Expand Down
67 changes: 51 additions & 16 deletions django_crypto_fields/cryptor.py
@@ -1,9 +1,11 @@
import binascii
import sys

from Crypto import Random
from Crypto.Cipher import AES as AES_CIPHER

from django.apps import apps as django_apps
from django.conf import settings
from django.core.exceptions import AppRegistryNotReady

from .constants import RSA, AES, PRIVATE, PUBLIC, ENCODING, style
Expand All @@ -16,49 +18,82 @@ class Cryptor(object):
The PEM file names and paths are in KEY_FILENAMES. KEYS is a copy of this except the
filenames are replaced with the actual keys."""

def __init__(self, keys=None):
def __init__(self, keys=None, aes_encryption_mode=None):
self.aes_encryption_mode = aes_encryption_mode
if not self.aes_encryption_mode:
try:
# do not use MODE_CFB, see comments in pycrypto.blockalgo.py
self.aes_encryption_mode = settings.AES_ENCRYPTION_MODE
except AttributeError:
self.aes_encryption_mode = AES_CIPHER.MODE_CBC
try:
# ignore "keys" parameter if Django is loaded
self.keys = django_apps.get_app_config('django_crypto_fields').encryption_keys
except AppRegistryNotReady:
self.keys = keys

def aes_encrypt(self, plaintext, mode):
def padded(self, plaintext, block_size):
"""Return string padded so length is a multiple of the block size.
* store length of padding the last hex value.
* if padding is 0, pad as if padding is 16.
* AES_CIPHER.MODE_CFB should not be used, but was used without padding
in the past. Continue to skip padding for this mode.
"""
try:
plaintext = plaintext.encode(ENCODING)
except AttributeError:
pass
attr = '_'.join([AES, mode, PRIVATE, 'key'])
aes_key = getattr(self.keys, attr)
if self.aes_encryption_mode == AES_CIPHER.MODE_CFB:
padding_length = 0
else:
padding_length = (block_size - len(plaintext) % block_size) % block_size
padding_length = padding_length or 16
padded = plaintext + (b'\x00' * (padding_length - 1)) + binascii.a2b_hex(str(padding_length).zfill(2))
if len(padded) % block_size > 0:
raise EncryptionError('Padding error, got padded string not a multiple of {}. Got {}'.format(
block_size, len(padded) / block_size))
return padded

def unpadded(self, plaintext, block_size):
"""Return original plaintext without padding.
Length of padding is stored in last two characters of plaintext."""
if self.aes_encryption_mode == AES_CIPHER.MODE_CFB:
return plaintext
padding_length = int(binascii.b2a_hex(plaintext[-1:]))
if not padding_length:
return plaintext[:-1]
return plaintext[:-padding_length]

def aes_encrypt(self, plaintext, mode):
aes_key = '_'.join([AES, mode, PRIVATE, 'key'])
iv = Random.new().read(AES_CIPHER.block_size)
cipher = AES_CIPHER.new(aes_key, AES_CIPHER.MODE_CFB, iv)
return iv + cipher.encrypt(plaintext)
cipher = AES_CIPHER.new(getattr(self.keys, aes_key), self.aes_encryption_mode, iv)
padded_plaintext = self.padded(plaintext, cipher.block_size)
return iv + cipher.encrypt(padded_plaintext)

def aes_decrypt(self, ciphertext, mode):
attr = '_'.join([AES, mode, PRIVATE, 'key'])
aes_key = getattr(self.keys, attr)
aes_key = '_'.join([AES, mode, PRIVATE, 'key'])
iv = ciphertext[:AES_CIPHER.block_size]
cipher = AES_CIPHER.new(aes_key, AES_CIPHER.MODE_CFB, iv)
cipher = AES_CIPHER.new(getattr(self.keys, aes_key), self.aes_encryption_mode, iv)
plaintext = cipher.decrypt(ciphertext)[AES_CIPHER.block_size:]
return plaintext.decode(ENCODING)
return self.unpadded(plaintext, cipher.block_size).decode()

def rsa_encrypt(self, plaintext, mode):
attr = '_'.join([RSA, mode, PUBLIC, 'key'])
rsa_key = getattr(self.keys, attr)
rsa_key = '_'.join([RSA, mode, PUBLIC, 'key'])
try:
plaintext = plaintext.encode(ENCODING)
except AttributeError:
pass
try:
ciphertext = rsa_key.encrypt(plaintext)
ciphertext = getattr(self.keys, rsa_key).encrypt(plaintext)
except (ValueError, TypeError) as e:
raise EncryptionError('RSA encryption failed for value. Got \'{}\''.format(e))
return ciphertext

def rsa_decrypt(self, ciphertext, mode):
attr = '_'.join([RSA, mode, PRIVATE, 'key'])
rsa_key = getattr(self.keys, attr)
plaintext = rsa_key.decrypt(ciphertext)
rsa_key = '_'.join([RSA, mode, PRIVATE, 'key'])
plaintext = getattr(self.keys, rsa_key).decrypt(ciphertext)
return plaintext.decode(ENCODING)

def test_rsa(self):
Expand Down

0 comments on commit e885432

Please sign in to comment.