diff --git a/.gitignore b/.gitignore index be63f75..6d70688 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ local_settings.py build *.sqlite3 *~ +venv* diff --git a/account/password_encryption.py b/account/password_encryption.py index 370fd3f..3fa9766 100644 --- a/account/password_encryption.py +++ b/account/password_encryption.py @@ -4,8 +4,8 @@ import base64 import os -from Crypto import Random -from Crypto.Cipher import AES +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend ENCRYPTED_LDAP_PASSWORD = 'encrypted_ldap_password' @@ -18,17 +18,15 @@ def encrypt_ldap_password(cleartext_pw): The key is supposed to be stored into the 'session_key' cookie field we can later use it to decrypt the password and connect to the LDAP server with it. """ - # 16 bytes of key => AES-128 - random = Random.new() - key = os.urandom(16) # random.read(16) - + key = os.urandom(16) # 128-bit AES key + iv = os.urandom(16) # 128-bit IV - # initialization vector - iv = os.urandom(16) # random.read(16) + cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=default_backend()) + encryptor = cipher.encryptor() - # do the encryption - aes = AES.new(key, AES.MODE_CFB, iv) - message = iv + aes.encrypt(cleartext_pw.encode()) + ciphertext = encryptor.update(cleartext_pw.encode()) + encryptor.finalize() + + message = iv + ciphertext return base64.b64encode(message).decode(), base64.b64encode(key).decode() @@ -40,16 +38,14 @@ def decrypt_ldap_password(message, key): decoded_message = base64.b64decode(message) decoded_key = base64.b64decode(key) - # first 16 bytes of the message are the initialization vector iv = decoded_message[:16] - - # the rest is the encrypted password ciphertext = decoded_message[16:] - # decrypt it - aes = AES.new(decoded_key, AES.MODE_CFB, iv) - cleartext_pw = aes.decrypt(ciphertext).decode() - return cleartext_pw + cipher = Cipher(algorithms.AES(decoded_key), modes.CFB(iv), backend=default_backend()) + decryptor = cipher.decryptor() + + cleartext_pw = decryptor.update(ciphertext) + decryptor.finalize() + return cleartext_pw.decode() def store_ldap_password(request, password): diff --git a/account/tests.py b/account/tests.py index 07c7e52..1a7b5e6 100644 --- a/account/tests.py +++ b/account/tests.py @@ -4,6 +4,8 @@ when you run "manage.py test". Replace this with more appropriate tests for your application. """ +import base64 +import pytest from django.test import TestCase from account.password_encryption import encrypt_ldap_password, \ @@ -28,3 +30,37 @@ class PasswordEncryptionTest(TestCase): message, key = self.encrypt_it() decrypted = decrypt_ldap_password(message, key) self.assertEqual(self.TEST_LDAP_PASSWD, decrypted) + +@pytest.mark.parametrize("password", [ + "simplePassword123", + "pässwörd_mit_üöäß", + "", + " " * 10, + "🔐✨🚀", +]) +def test_encrypt_decrypt_roundtrip(password): + encrypted, key = encrypt_ldap_password(password) + + encrypted_bytes = base64.b64decode(encrypted) + key_bytes = base64.b64decode(key) + + assert isinstance(encrypted, str) + assert isinstance(key, str) + assert len(key_bytes) == 16 # 128-bit AES + + decrypted = decrypt_ldap_password(encrypted, key) + assert decrypted == password + + +def test_decryption_with_wrong_key_should_fail(): + password = "correctPassword" + encrypted, key = encrypt_ldap_password(password) + + wrong_key_bytes = base64.b64decode(key) + wrong_key_bytes = bytearray(wrong_key_bytes) + wrong_key_bytes[0] ^= 0xFF # Flip first bit + wrong_key = base64.b64encode(bytes(wrong_key_bytes)).decode() + + with pytest.raises(Exception): + decrypt_ldap_password(encrypted, wrong_key) + diff --git a/cbmi/settings.py b/cbmi/settings.py index 1abfdf2..175ddd0 100644 --- a/cbmi/settings.py +++ b/cbmi/settings.py @@ -204,11 +204,9 @@ INSTALLED_APPS = ( 'django.contrib.staticfiles', 'django.contrib.admin', 'django.contrib.admindocs', - # 'jsonrpc', # STUBBED due to django-1.8.4 upgraded - 'crispy_forms', - # 'cbmi', + "crispy_forms", + "crispy_bootstrap4", 'account', - # 'cbapi_ldap', ) # A sample logging configuration. The only tangible logging @@ -242,7 +240,8 @@ LOGGING = { } } -CRISPY_TEMPLATE_PACK = 'bootstrap' +CRISPY_TEMPLATE_PACK = "bootstrap4" +CRISPY_FAIL_SILENTLY = False # c-base specific settings CBASE_LDAP_URL = 'ldaps://lea.cbrp3.c-base.org:389/' diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..4d7860c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +# pytest.ini +[pytest] +DJANGO_SETTINGS_MODULE = cbmi.settings +python_files = tests.py test_*.py *_tests.py + diff --git a/requirements.in b/requirements.in index 96076dc..e722059 100644 --- a/requirements.in +++ b/requirements.in @@ -1,7 +1,12 @@ +crispy-bootstrap4 +cryptography Django<5 django-auth-ldap django-crispy-forms +django-bootstrap4 gunicorn -pycrypto +# pycrypto passlib +pytest +pytest-django requests diff --git a/requirements.txt b/requirements.txt index ce08287..0d491f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,45 +1,67 @@ # -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: # -# pip-compile requirements.in +# pip-compile # -asgiref==3.5.2 +asgiref==3.8.1 # via django -certifi==2022.5.18.1 +certifi==2025.1.31 # via requests -charset-normalizer==2.0.12 +cffi==1.17.1 + # via cryptography +charset-normalizer==3.4.1 # via requests -django==4.0.5 +# cryptography==41.0.7 + # via -r requirements.in +django==4.2.20 # via # -r requirements.in # django-auth-ldap -django-auth-ldap==4.1.0 + # django-crispy-forms +django-auth-ldap==5.1.0 # via -r requirements.in -django-crispy-forms==1.14.0 +django-crispy-forms==2.3 # via -r requirements.in -gunicorn==20.1.0 +exceptiongroup==1.2.2 + # via pytest +gunicorn==23.0.0 # via -r requirements.in -idna==3.3 +idna==3.10 # via requests +iniconfig==2.1.0 + # via pytest +packaging==24.2 + # via + # gunicorn + # pytest passlib==1.7.4 # via -r requirements.in -pyasn1==0.4.8 +pluggy==1.5.0 + # via pytest +pyasn1==0.6.1 # via # pyasn1-modules # python-ldap -pyasn1-modules==0.2.8 +pyasn1-modules==0.4.2 # via python-ldap -pycrypto==2.6.1 +pycparser==2.22 + # via cffi +pytest==8.3.5 + # via + # -r requirements.in + # pytest-django +pytest-django==4.11.1 # via -r requirements.in -python-ldap==3.4.0 +python-ldap==3.4.4 # via django-auth-ldap -requests==2.28.0 +requests==2.32.3 # via -r requirements.in -sqlparse==0.4.2 +sqlparse==0.5.3 # via django -urllib3==1.26.9 +tomli==2.2.1 + # via pytest +typing-extensions==4.13.2 + # via asgiref +urllib3==2.4.0 # via requests - -# The following packages are considered to be unsafe in a requirements file: -# setuptools