From a25b69b8a6cc5b21c9bbda637ad4df8b6f464a69 Mon Sep 17 00:00:00 2001 From: Jim Laney Date: Mon, 5 Feb 2024 13:19:26 -0800 Subject: [PATCH 1/6] add test matrix for python versions --- .github/workflows/cicd.yml | 22 ++++++++++++++-------- blti/__init__.py | 2 +- blti/crypto.py | 2 +- blti/middleware.py | 2 +- blti/models.py | 2 +- blti/performance.py | 2 +- blti/tests.py | 2 +- blti/urls.py | 2 +- blti/validators.py | 2 +- blti/views/__init__.py | 2 +- blti/views/develop.py | 2 +- setup.py | 3 +-- 12 files changed, 25 insertions(+), 20 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 018c4e1..820febc 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -28,7 +28,8 @@ name: tests env: APP_NAME: blti CONF_PATH: conf - COVERAGE_DJANGO_VERSION: 3.2 + COVERAGE_DJANGO_VERSION: '4.2' + COVERAGE_PYTHON_VERSION: '3.10' on: push: @@ -42,10 +43,13 @@ on: jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: + python-version: + - '3.8' + - '3.10' django-version: - '3.2' - '4.2' @@ -54,10 +58,10 @@ jobs: - name: Checkout Repo uses: actions/checkout@v3 - - name: Setup Python + - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: ${{ matrix.python-version }} - name: Install Dependencies run: | @@ -66,7 +70,7 @@ jobs: pip install -e . pip install coverage coveralls==3.3.1 - - name: Upgrade Django Version + - name: Upgrade Django ${{ matrix.django-version }} run: pip install "Django~=${{ matrix.django-version }}.0" - name: Setup Django @@ -90,7 +94,9 @@ jobs: coverage run --source=${APP_NAME}/ manage.py test ${APP_NAME} - name: Report Test Coverage - if: matrix.django-version == env.COVERAGE_DJANGO_VERSION + if: | + matrix.django-version == env.COVERAGE_DJANGO_VERSION && + matrix.python-version == env.COVERAGE_PYTHON_VERSION env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash @@ -101,7 +107,7 @@ jobs: needs: test - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout Repo @@ -110,7 +116,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: '3.10' - name: Publish to PyPi uses: uw-it-aca/actions/publish-pypi@main diff --git a/blti/__init__.py b/blti/__init__.py index e8ab890..46efbe2 100644 --- a/blti/__init__.py +++ b/blti/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2023 UW-IT, University of Washington +# Copyright 2024 UW-IT, University of Washington # SPDX-License-Identifier: Apache-2.0 diff --git a/blti/crypto.py b/blti/crypto.py index 5102921..4e94f25 100644 --- a/blti/crypto.py +++ b/blti/crypto.py @@ -1,4 +1,4 @@ -# Copyright 2023 UW-IT, University of Washington +# Copyright 2024 UW-IT, University of Washington # SPDX-License-Identifier: Apache-2.0 diff --git a/blti/middleware.py b/blti/middleware.py index 007c0a4..2529b75 100644 --- a/blti/middleware.py +++ b/blti/middleware.py @@ -1,4 +1,4 @@ -# Copyright 2023 UW-IT, University of Washington +# Copyright 2024 UW-IT, University of Washington # SPDX-License-Identifier: Apache-2.0 diff --git a/blti/models.py b/blti/models.py index 799b35d..51dbcd1 100644 --- a/blti/models.py +++ b/blti/models.py @@ -1,4 +1,4 @@ -# Copyright 2023 UW-IT, University of Washington +# Copyright 2024 UW-IT, University of Washington # SPDX-License-Identifier: Apache-2.0 diff --git a/blti/performance.py b/blti/performance.py index 794ec59..729fabd 100644 --- a/blti/performance.py +++ b/blti/performance.py @@ -1,4 +1,4 @@ -# Copyright 2023 UW-IT, University of Washington +# Copyright 2024 UW-IT, University of Washington # SPDX-License-Identifier: Apache-2.0 diff --git a/blti/tests.py b/blti/tests.py index 11c35bd..105fca2 100644 --- a/blti/tests.py +++ b/blti/tests.py @@ -1,4 +1,4 @@ -# Copyright 2023 UW-IT, University of Washington +# Copyright 2024 UW-IT, University of Washington # SPDX-License-Identifier: Apache-2.0 diff --git a/blti/urls.py b/blti/urls.py index 93e01b1..ca843b7 100644 --- a/blti/urls.py +++ b/blti/urls.py @@ -1,4 +1,4 @@ -# Copyright 2023 UW-IT, University of Washington +# Copyright 2024 UW-IT, University of Washington # SPDX-License-Identifier: Apache-2.0 diff --git a/blti/validators.py b/blti/validators.py index 0d22cd0..bf3eec6 100644 --- a/blti/validators.py +++ b/blti/validators.py @@ -1,4 +1,4 @@ -# Copyright 2023 UW-IT, University of Washington +# Copyright 2024 UW-IT, University of Washington # SPDX-License-Identifier: Apache-2.0 diff --git a/blti/views/__init__.py b/blti/views/__init__.py index f4ca572..7b9034e 100644 --- a/blti/views/__init__.py +++ b/blti/views/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2023 UW-IT, University of Washington +# Copyright 2024 UW-IT, University of Washington # SPDX-License-Identifier: Apache-2.0 diff --git a/blti/views/develop.py b/blti/views/develop.py index 4d205f4..425672e 100644 --- a/blti/views/develop.py +++ b/blti/views/develop.py @@ -1,4 +1,4 @@ -# Copyright 2023 UW-IT, University of Washington +# Copyright 2024 UW-IT, University of Washington # SPDX-License-Identifier: Apache-2.0 diff --git a/setup.py b/setup.py index eada27f..c32771f 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ description='A Django Application on which to build IMS BLTI Tool Providers', long_description=README, url='https://github.com/uw-it-aca/django-blti', - author="UW-IT AXDD", + author="UW-IT T&LS", author_email="aca-it@uw.edu", classifiers=[ 'Environment :: Web Environment', @@ -37,6 +37,5 @@ 'License :: OSI Approved :: Apache Software License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.8', ], ) From c4a35ff43211c866286d5438aeb48f373aa93bfe Mon Sep 17 00:00:00 2001 From: Jim Laney Date: Mon, 5 Feb 2024 13:22:58 -0800 Subject: [PATCH 2/6] drop apt install --- .github/workflows/cicd.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 820febc..7caaeb0 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -65,7 +65,6 @@ jobs: - name: Install Dependencies run: | - sudo apt-get install python-dev libxml2-dev libxmlsec1-dev python -m pip install --upgrade pip pip install -e . pip install coverage coveralls==3.3.1 From 8331a57c2e91f08ca1b1a92f9ceb04d90390774b Mon Sep 17 00:00:00 2001 From: Jim Laney Date: Mon, 5 Feb 2024 15:26:27 -0800 Subject: [PATCH 3/6] drop pycrypto --- blti/crypto.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/blti/crypto.py b/blti/crypto.py index 4e94f25..5bf5ac1 100644 --- a/blti/crypto.py +++ b/blti/crypto.py @@ -20,12 +20,12 @@ def __init__(self, key, iv): if key is None: raise ValueError('Missing AES key') else: - self._key = key + self._key = key.encode('utf8') if iv is None: raise ValueError('Missing AES initialization vector') else: - self._iv = iv + self._iv = iv.encode('utf8') def encrypt(self, msg): msg = self._pad(self.str_to_bytes(msg)) diff --git a/setup.py b/setup.py index c32771f..1463a8d 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ install_requires=[ 'Django>=3.2,<5', 'oauthlib', - 'PyCrypto', + 'pycryptodome', 'mock', ], license='Apache License, Version 2.0', From 1db6a54a5ef22e5122f41afc0c4690b3a4110efd Mon Sep 17 00:00:00 2001 From: Jim Laney Date: Mon, 5 Feb 2024 16:17:43 -0800 Subject: [PATCH 4/6] cryptography implementation for handling lti message --- blti/crypto.py | 31 +++++++++++++++++++++++-------- setup.py | 2 +- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/blti/crypto.py b/blti/crypto.py index 5bf5ac1..83d1805 100644 --- a/blti/crypto.py +++ b/blti/crypto.py @@ -2,11 +2,17 @@ # SPDX-License-Identifier: Apache-2.0 -from Crypto.Cipher import AES +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from base64 import b64decode, b64encode + class aes128cbc(object): + """ + https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/ + https://cryptography.io/en/latest/hazmat/primitives/padding/ + """ _key = None _iv = None @@ -28,21 +34,30 @@ def __init__(self, key, iv): self._iv = iv.encode('utf8') def encrypt(self, msg): + cipher = Cipher(algorithms.AES256(self._key), modes.CBC(self._iv)) + encryptor = cipher.encryptor() msg = self._pad(self.str_to_bytes(msg)) - crypt = AES.new(self._key, AES.MODE_CBC, self._iv) - return b64encode(crypt.encrypt(msg)).decode('utf-8') + ct = encryptor.update(msg) + encryptor.finalize() + return ct def decrypt(self, msg): msg = b64decode(msg) - crypt = AES.new(self._key, AES.MODE_CBC, self._iv) - return self._unpad(crypt.decrypt(msg)).decode('utf-8') + cipher = Cipher(algorithms.AES(self._key), modes.CBC(self._iv)) + decryptor = cipher.decryptor() + dct = decryptor.update(msg) + decryptor.finalize() + return self._unpad(dct).decode('utf-8') def _pad(self, s): - return s + (self._bs - len(s) % self._bs) * self.str_to_bytes(chr( - self._bs - len(s) % self._bs)) + padder = padding.PKCS7(self._bs).padder() + return padder.update(s) + padder.finalize() + + #return s + (self._bs - len(s) % self._bs) * self.str_to_bytes(chr( + # self._bs - len(s) % self._bs)) def _unpad(self, s): - return s[:-ord(s[len(s)-1:])] + unpadder = padding.PKCS7(self._bs).unpadder() + return unpadder.update(padded_data) + unpadder.finalize() + #return s[:-ord(s[len(s)-1:])] def str_to_bytes(self, s): u_type = type(b''.decode('utf8')) diff --git a/setup.py b/setup.py index 1463a8d..47153a7 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ install_requires=[ 'Django>=3.2,<5', 'oauthlib', - 'pycryptodome', + 'cryptography', 'mock', ], license='Apache License, Version 2.0', From 8246e51a1791e85c5023b72a7ef872674d7f0e75 Mon Sep 17 00:00:00 2001 From: Jim Laney Date: Tue, 6 Feb 2024 09:28:28 -0800 Subject: [PATCH 5/6] use cryptography instead of pycrypto --- blti/crypto.py | 50 ++++++++++++++++++-------------------------------- blti/tests.py | 16 ++++++++++++++-- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/blti/crypto.py b/blti/crypto.py index 83d1805..4444e80 100644 --- a/blti/crypto.py +++ b/blti/crypto.py @@ -3,64 +3,50 @@ from cryptography.hazmat.primitives import padding -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.ciphers import Cipher, modes +from cryptography.hazmat.primitives.ciphers.algorithms import AES from base64 import b64decode, b64encode - class aes128cbc(object): """ + Advanced Encryption Standard object + + For reference: https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/ - https://cryptography.io/en/latest/hazmat/primitives/padding/ """ _key = None _iv = None def __init__(self, key, iv): - """ - Advanced Encryption Standard object - """ - self._bs = 16 # Block size - if key is None: raise ValueError('Missing AES key') - else: - self._key = key.encode('utf8') - if iv is None: raise ValueError('Missing AES initialization vector') - else: - self._iv = iv.encode('utf8') + + self._key = key.encode('utf8') + self._iv = iv.encode('utf8') def encrypt(self, msg): - cipher = Cipher(algorithms.AES256(self._key), modes.CBC(self._iv)) + msg = msg.encode('utf8') + cipher = Cipher(AES(self._key), modes.CBC(self._iv)) encryptor = cipher.encryptor() - msg = self._pad(self.str_to_bytes(msg)) - ct = encryptor.update(msg) + encryptor.finalize() + ct = encryptor.update(self._pad(msg)) + encryptor.finalize() return ct def decrypt(self, msg): - msg = b64decode(msg) - cipher = Cipher(algorithms.AES(self._key), modes.CBC(self._iv)) + cipher = Cipher(AES(self._key), modes.CBC(self._iv)) decryptor = cipher.decryptor() dct = decryptor.update(msg) + decryptor.finalize() return self._unpad(dct).decode('utf-8') def _pad(self, s): - padder = padding.PKCS7(self._bs).padder() - return padder.update(s) + padder.finalize() - - #return s + (self._bs - len(s) % self._bs) * self.str_to_bytes(chr( - # self._bs - len(s) % self._bs)) + padder = padding.PKCS7(AES.block_size).padder() + pd = padder.update(s) + padder.finalize() + return pd def _unpad(self, s): - unpadder = padding.PKCS7(self._bs).unpadder() - return unpadder.update(padded_data) + unpadder.finalize() - #return s[:-ord(s[len(s)-1:])] - - def str_to_bytes(self, s): - u_type = type(b''.decode('utf8')) - if isinstance(s, u_type): - return s.encode('utf8') - return s + unpadder = padding.PKCS7(AES.block_size).unpadder() + upd = unpadder.update(s) + unpadder.finalize() + return upd diff --git a/blti/tests.py b/blti/tests.py index 105fca2..0c5c962 100644 --- a/blti/tests.py +++ b/blti/tests.py @@ -210,13 +210,25 @@ def test_get_session(self): self.assertEqual(blti.get_session(self.request), {}) def test_encrypt_decrypt_session(self): + blti = BLTI() data = {'abc': {'key': 123}, 'xyz': ('LTI provides a framework through which an LMS ' 'can send some verifiable information about a ' 'user to a third party.')} - enc = BLTI()._encrypt_session(data) - self.assertEquals(BLTI()._decrypt_session(enc), data) + enc = blti._encrypt_session(data) + self.assertEquals(blti._decrypt_session(enc), data) + + bdata = b'abcdef' + self.assertRaises(TypeError, blti._encrypt_session, bdata) + + def test_missing_key_iv(self): + blti = BLTI() + with override_settings(BLTI_AES_KEY=None): + self.assertRaises(ValueError, blti._encrypt_session, '') + + with override_settings(BLTI_AES_IV=None): + self.assertRaises(ValueError, blti._encrypt_session, '') def test_filter_oauth_params(self): data = getattr(settings, 'CANVAS_LTI_V1_LAUNCH_PARAMS', {}) From 01c8ccf514d704acdada3e0fb68f250b3343b559 Mon Sep 17 00:00:00 2001 From: Jim Laney Date: Tue, 6 Feb 2024 09:38:38 -0800 Subject: [PATCH 6/6] re-add str_to_bytes --- blti/crypto.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/blti/crypto.py b/blti/crypto.py index 4444e80..497c19f 100644 --- a/blti/crypto.py +++ b/blti/crypto.py @@ -29,10 +29,10 @@ def __init__(self, key, iv): self._iv = iv.encode('utf8') def encrypt(self, msg): - msg = msg.encode('utf8') + msg = self._pad(self.str_to_bytes(msg)) cipher = Cipher(AES(self._key), modes.CBC(self._iv)) encryptor = cipher.encryptor() - ct = encryptor.update(self._pad(msg)) + encryptor.finalize() + ct = encryptor.update(msg) + encryptor.finalize() return ct def decrypt(self, msg): @@ -50,3 +50,9 @@ def _unpad(self, s): unpadder = padding.PKCS7(AES.block_size).unpadder() upd = unpadder.update(s) + unpadder.finalize() return upd + + def str_to_bytes(self, s): + u_type = type(b''.decode('utf8')) + if isinstance(s, u_type): + return s.encode('utf8') + return s