diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 03d27dd..618640c 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -1,33 +1,37 @@ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI on: push: - branches: [ master ] + branches: [ main ] paths: - 'nodejs/**' - .github/workflows/node.yml pull_request: - branches: [ master ] + branches: [ main ] paths: - 'nodejs/**' + - .github/workflows/node.yml -jobs: - build: +permissions: + contents: read +jobs: + test: runs-on: ubuntu-latest strategy: matrix: - node-version: [16, 18, 20] + node-version: [ 20, 22, 24 ] steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - run: cd nodejs; npm ci - - run: cd nodejs; node test.js dump verbose + - uses: actions/checkout@v6 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: cd nodejs && npm install + - name: Run tests + run: cd nodejs && npm run test:verbose diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 69bf2b9..ad1e921 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -1,41 +1,52 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python package on: push: - branches: [ master ] + branches: [ main ] paths: - 'python/**' - .github/workflows/python.yml pull_request: - branches: [ master ] + branches: [ main ] paths: - 'python/**' + - .github/workflows/python.yml -jobs: - build: +permissions: + contents: read +jobs: + test: runs-on: ubuntu-latest + strategy: matrix: - python-version: ['3.7', '3.8', '3.9'] + python-version: [ + '3.10', + '3.11', + '3.12', + '3.13', + '3.14', + ] steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install black nose - cd python; pip install -r requirements.txt - - name: Reformat - run: | - cd python; black --check . - - name: Run tests - run: | - cd python; nosetests + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + cd python && pip install .[dev] + - name: Run tests + run: cd python && tox + env: + TOX_GH_MAJOR_MINOR: ${{ matrix.python-version }} + - uses: astral-sh/ruff-action@v3 + with: + src: "./python" + - name: Check formatting + run: cd python && black --check . diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json deleted file mode 100644 index cee076d..0000000 --- a/nodejs/package-lock.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "http_ece", - "version": "1.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "http_ece", - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">=16" - } - } - } -} diff --git a/nodejs/package.json b/nodejs/package.json index 45fbc44..000b80b 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -19,9 +19,10 @@ "license": "MIT", "main": "./ece.js", "scripts": { - "test": "node ./test.js" + "test": "node ./test.js", + "test:verbose": "node ./test.js dump verbose" }, "engines": { - "node": ">=16" + "node": ">=20" } } diff --git a/python/.coveragerc b/python/.coveragerc deleted file mode 100644 index cc9c803..0000000 --- a/python/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[report] -show_missing = true diff --git a/python/http_ece/__init__.py b/python/http_ece/__init__.py index d69263a..b1f1435 100644 --- a/python/http_ece/__init__.py +++ b/python/http_ece/__init__.py @@ -5,10 +5,10 @@ from cryptography.exceptions import InvalidTag from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat -from cryptography.hazmat.primitives.asymmetric import ec MAX_RECORD_SIZE = pow(2, 31) - 1 MIN_RECORD_SIZE = 3 @@ -304,7 +304,7 @@ def unpad(data, last): result += unpad_legacy(data) counter += 1 except InvalidTag as ex: - raise ECEException("Decryption error: {}".format(repr(ex))) + raise ECEException(f"Decryption error: {ex!r}") return result diff --git a/python/http_ece/tests/test_ece.py b/python/http_ece/tests/test_ece.py index 1eb6c58..0ed980e 100644 --- a/python/http_ece/tests/test_ece.py +++ b/python/http_ece/tests/test_ece.py @@ -3,17 +3,17 @@ import os import struct import unittest +from pathlib import Path + +import pytest from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat -from pytest import raises - import http_ece as ece from http_ece import ECEException - -TEST_VECTORS = os.path.join(os.sep, "..", "encrypt_data.json")[1:] +TEST_VECTORS = Path(__file__).parent.parent / "encrypt_data.json" def logmsg(arg): @@ -59,7 +59,7 @@ def setUp(self): self.m_salt = os.urandom(16) def test_derive_key_invalid_mode(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="unknown 'mode' specified: invalid"): ece.derive_key( "invalid", version="aes128gcm", @@ -70,10 +70,9 @@ def test_derive_key_invalid_mode(self): auth_secret=None, keyid="valid", ) - assert ex.value.message == "unknown 'mode' specified: invalid" def test_derive_key_invalid_salt(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="'salt' must be a 16 octet value"): ece.derive_key( "encrypt", version="aes128gcm", @@ -84,10 +83,9 @@ def test_derive_key_invalid_salt(self): auth_secret=None, keyid="valid", ) - assert ex.value.message == "'salt' must be a 16 octet value" def test_derive_key_invalid_version(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="Invalid version"): ece.derive_key( "encrypt", version="invalid", @@ -98,10 +96,9 @@ def test_derive_key_invalid_version(self): auth_secret=None, keyid="valid", ) - assert ex.value.message == "Invalid version" def test_derive_key_no_private_key(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="DH requires a private_key"): ece.derive_key( "encrypt", version="aes128gcm", @@ -112,10 +109,9 @@ def test_derive_key_no_private_key(self): auth_secret=None, keyid="valid", ) - assert ex.value.message == "DH requires a private_key" def test_derive_key_no_secret(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="unable to determine the secret"): ece.derive_key( "encrypt", version="aes128gcm", @@ -126,12 +122,10 @@ def test_derive_key_no_secret(self): auth_secret=None, keyid="valid", ) - assert ex.value.message == "unable to determine the secret" def test_iv_bad_counter(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="Counter too big"): ece.iv(os.urandom(8), pow(2, 64) + 1) - assert ex.value.message == "Counter too big" class TestEceChecking(unittest.TestCase): @@ -144,65 +138,59 @@ def setUp(self): self.m_header += struct.pack("!L", 32) + b"\0" def test_encrypt_small_rs(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="Record size too small"): ece.encrypt( self.m_input, version="aes128gcm", key=self.m_key, rs=1, ) - assert ex.value.message == "Record size too small" def test_decrypt_small_rs(self): header = os.urandom(16) + struct.pack("!L", 2) + b"\0" - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="Record size too small"): ece.decrypt( header + self.m_input, version="aes128gcm", key=self.m_key, rs=1, ) - assert ex.value.message == "Record size too small" def test_encrypt_bad_version(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="Invalid version"): ece.encrypt( self.m_input, version="bogus", key=self.m_key, ) - assert ex.value.message == "Invalid version" def test_decrypt_bad_version(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="Invalid version"): ece.decrypt( self.m_input, version="bogus", key=self.m_key, ) - assert ex.value.message == "Invalid version" def test_decrypt_bad_header(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="Could not parse the content header"): ece.decrypt( os.urandom(4), version="aes128gcm", key=self.m_key, ) - assert ex.value.message == "Could not parse the content header" def test_encrypt_long_keyid(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="keyid is too long"): ece.encrypt( self.m_input, version="aes128gcm", key=self.m_key, keyid=b64e(os.urandom(192)), # 256 bytes ) - assert ex.value.message == "keyid is too long" def test_overlong_padding(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="all zero record plaintext"): ece.decrypt( self.m_header + b"\xbb\xc7\xb9ev\x0b\xf0f+\x93\xf4" b"\xe5\xd6\x94\xb7e\xf0\xcd\x15\x9b(\x01\xa5", @@ -210,10 +198,9 @@ def test_overlong_padding(self): key=b"d\xc7\x0ed\xa7%U\x14Q\xf2\x08\xdf\xba\xa0\xb9r", keyid=b64e(os.urandom(192)), # 256 bytes ) - assert ex.value.message == "all zero record plaintext" def test_bad_early_delimiter(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="record delimiter != 1"): ece.decrypt( self.m_header + b"\xb9\xc7\xb9ev\x0b\xf0\x9eB\xb1\x08C8u" b"\xa3\x06\xc9x\x06\n\xfc|}\xe9R\x85\x91" @@ -224,10 +211,9 @@ def test_bad_early_delimiter(self): key=b"d\xc7\x0ed\xa7%U\x14Q\xf2\x08\xdf\xba\xa0\xb9r", keyid=b64e(os.urandom(192)), # 256 bytes ) - assert ex.value.message == "record delimiter != 1" def test_bad_final_delimiter(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="last record delimiter != 2"): ece.decrypt( self.m_header + b"\xba\xc7\xb9ev\x0b\xf0\x9eB\xb1\x08Ji" b"\xe4P\x1b\x8dI\xdb\xc6y#MG\xc2W\x16", @@ -235,10 +221,9 @@ def test_bad_final_delimiter(self): key=b"d\xc7\x0ed\xa7%U\x14Q\xf2\x08\xdf\xba\xa0\xb9r", keyid=b64e(os.urandom(192)), # 256 bytes ) - assert ex.value.message == "last record delimiter != 2" def test_damage(self): - with raises(ECEException) as ex: + with pytest.raises(ECEException, match=r"Decryption error: InvalidTag()"): ece.decrypt( self.m_header + b"\xbb\xc6\xb1\x1dF:~\x0f\x07+\xbe\xaaD" b"\xe0\xd6.K\xe5\xf9]%\xe3\x86q\xe0}", @@ -246,7 +231,6 @@ def test_damage(self): key=b"d\xc7\x0ed\xa7%U\x14Q\xf2\x08\xdf\xba\xa0\xb9r", keyid=b64e(os.urandom(192)), # 256 bytes ) - assert ex.value.message == "Decryption error: InvalidTag()" class TestEceIntegration(unittest.TestCase): @@ -350,9 +334,8 @@ def detect_truncation(self, version): chunk = encrypted[0 : 21 + rs] else: chunk = encrypted[0 : rs + 16] - with raises(ECEException) as ex: + with pytest.raises(ECEException, match="Message truncated"): ece.decrypt(chunk, salt=salt, key=key, rs=rs, version=version) - assert ex.value.message == "Message truncated" def use_dh(self, version): def pubbytes(k): @@ -427,9 +410,9 @@ class TestNode(unittest.TestCase): """Testing using data from the node.js version.""" def setUp(self): - if not os.path.exists(TEST_VECTORS): - self.skipTest("No %s file found" % TEST_VECTORS) - f = open(TEST_VECTORS, "r") + if not Path(TEST_VECTORS).exists(): + self.skipTest(f"No {TEST_VECTORS} file found") + f = Path(TEST_VECTORS).open() self.legacy_data = json.loads(f.read()) f.close() @@ -446,7 +429,7 @@ def _run(self, mode): outp = "input" for data in self.legacy_data: - logmsg("%s: %s" % (mode, data["test"])) + logmsg("{}: {}".format(mode, data["test"])) p = data["params"][mode] if "pad" in p and mode == "encrypt": diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..dd27d91 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,70 @@ +[build-system] +requires = ["setuptools >= 77.0.3"] +build-backend = "setuptools.build_meta" + +[project] +name = "http_ece" +version = "1.2.1" +license = "MIT" +authors = [ + { name = "Martin Thomson", email = "martin.thomson@gmail.com" } +] +keywords = ["crypto http"] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +description = "Encrypted Content Encoding for HTTP" +readme = { file = "README.rst", content-type = "text/x-rst" } +requires-python = ">=3.10" +dependencies = [ + "cryptography>=45.0.1" +] + +[project.optional-dependencies] +dev = [ + "black==25.12.0", + "pytest", + "pytest-cov", + "ruff==0.14.10", + "tox", + "tox-gh", +] + +[project.urls] +Repository = "https://github.com/web-push-libs/encrypted-content-encoding" + +[tool.distutils.bdist_wheel] +universal = 1 + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +addopts = "--verbose --cov=http_ece" +log_format = "%(asctime)s,%(msecs)03d %(name)s: %(levelname)s: %(message)s" +log_file_date_format = "%H:%M:%S" + +[tool.ruff] +lint.select = [ + "A", + "F", + "FURB", + "I", + "PERF", + "PT", + "PTH", + "RUF", + "S", + "UP", +] +[tool.ruff.lint.extend-per-file-ignores] +"**/tests/**" = [ + "A001", + "A002", + "S101", +] diff --git a/python/pytest.ini b/python/pytest.ini deleted file mode 100644 index c105b53..0000000 --- a/python/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -addopts= --verbose --verbosity=1 --cov=http_ece -log_format = %(asctime)s,%(msecs)03d %(name)s: %(levelname)s: %(message)s -log_file_date_format = %H:%M:%S diff --git a/python/requirements.txt b/python/requirements.txt deleted file mode 100644 index d6e1198..0000000 --- a/python/requirements.txt +++ /dev/null @@ -1 +0,0 @@ --e . diff --git a/python/setup.cfg b/python/setup.cfg deleted file mode 100644 index 3c6e79c..0000000 --- a/python/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 diff --git a/python/setup.py b/python/setup.py deleted file mode 100755 index 3807e2f..0000000 --- a/python/setup.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/python -import io -import os - -from setuptools import setup - -here = os.path.abspath(os.path.dirname(__file__)) -with io.open(os.path.join(here, "README.rst"), encoding="utf8") as f: - README = f.read() - -setup( - name="http_ece", - version="1.2.1", - author="Martin Thomson", - author_email="martin.thomson@gmail.com", - scripts=[], - packages=["http_ece"], - description="Encrypted Content Encoding for HTTP", - long_description="Encipher HTTP Messages", - classifiers=[ - "Development Status :: 4 - Beta", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], - keywords="crypto http", - install_requires=[ - "cryptography>=2.5", - ], - tests_require=[ - "pytest", - "pytest-cov", - ], - url="https://github.com/martinthomson/encrypted-content-encoding", - license="MIT", -) diff --git a/python/tox.ini b/python/tox.ini index 8be121d..800e88b 100644 --- a/python/tox.ini +++ b/python/tox.ini @@ -4,18 +4,24 @@ # and then run "tox" from this directory. [tox] -envlist = py27,py34,py35,py38,py39,py310,py311,py312 +envlist = py310,py311,py312,py313,py314 + +[gh] +; Github Action +python = + 3.10 = py310 + 3.11 = py311 + 3.12 = py312 + 3.13 = py313 + 3.14 = py314 [testenv] basepython = - py27: python2.7 - py34: python3.4 - py35: python3.5 - py38: python3.8 - py39: python3.9 py310: python3.10 py311: python3.11 py312: python3.12 + py313: python3.13 + py314: python3.14 commands = pytest \ []