├── psec ├── py.typed ├── __init__.py ├── cvv.py ├── tools.py ├── aes.py ├── des.py ├── pin.py ├── mac.py ├── pinblock.py └── tr31.py ├── tests ├── __init__.py ├── test_cvv.py ├── test_aes.py ├── test_des.py ├── test_mac.py ├── test_pin.py ├── test_pinblock.py └── test_tr31.py ├── requirements.txt ├── MANIFEST.in ├── requirements-dev.txt ├── pyproject.toml ├── CHANGELOG.rst ├── setup.cfg ├── LICENSE.md ├── .github └── workflows │ ├── coverage.yml │ └── test.yml ├── Makefile ├── README.rst ├── setup.py └── .gitignore /psec/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography>=1.0 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.md 3 | include requirements*.txt 4 | include pyproject.toml 5 | include setup.cfg 6 | include Makefile 7 | recursive-include tests *.py -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black == 24.3.0 2 | flake8 3 | pytest >= 6.0 # Min to support pyproject.toml 4 | pytest-cov 5 | coverage 6 | toml # Needed by coverage to support pyproject.toml 7 | codecov 8 | twine 9 | wheel 10 | setuptools 11 | mypy 12 | types-cryptography # Needed by mypy 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 40.6.2", 4 | "wheel >= 0.30.0", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [tool.pytest.ini_options] 9 | minversion = "6.0" 10 | addopts = "-ra --doctest-modules" 11 | doctest_optionflags = "NORMALIZE_WHITESPACE" 12 | testpaths = [ 13 | "tests", 14 | "psec", 15 | ] 16 | 17 | [tool.coverage.run] 18 | branch = true 19 | source = [ 20 | "psec", 21 | ] 22 | omit = [ 23 | "tests/*", 24 | "setup.py", 25 | ] 26 | 27 | [tool.coverage.report] 28 | show_missing = true 29 | 30 | [tool.mypy] 31 | strict = true 32 | files = [ 33 | "psec/**/*.py", 34 | ] 35 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 1.3.0 - 2022-12-06 2 | ------------------ 3 | - Add PIN block ISO 4 support. Thanks David Schmid . 4 | 5 | 1.2.0 - 2022-10-02 6 | ------------------ 7 | - Update TR-31 to wrap/unwrap any key. 8 | 9 | 1.1.0 - 2021-06-28 10 | ------------------ 11 | - Added support for TR-31 key block. Specification version 2018. 12 | 13 | 1.0.1 - 2021-02-28 14 | ------------------ 15 | Removed leftover code from PVV generation. 16 | 17 | 1.0.0 - 2021-02-19 18 | ------------------ 19 | - Card Verification Value generation 20 | - DES utilities 21 | - MAC ISO 9797 generation 22 | - PIN block encoding and decoding 23 | - IBM 3624 PIN and offset generation 24 | - Visa PIN Verification Value generation 25 | -------------------------------------------------------------------------------- /psec/__init__.py: -------------------------------------------------------------------------------- 1 | r"""psec is a payment security package for protecting sensitive data 2 | for retail payment transactions and cardholder authentication. 3 | 4 | psec modules: 5 | 6 | - tr31 - TR-31 key block wrapping and unwrapping 7 | - aes - Advanced Encryption Standard 8 | - des - Triple DES 9 | - cvv - Card Verification Value 10 | - mac - Message Authentication Code 11 | - pin - Personal Identification Number 12 | - pinblock - PIN Blocks encoding and decoding 13 | - tools - Miscellaneous tools, such as xor. 14 | """ 15 | 16 | __version__ = "1.3.0" 17 | __author__ = "Konstantin Novichikhin " 18 | 19 | from psec import aes, cvv, des, mac, pin, pinblock, tools, tr31 20 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | venv 4 | build 5 | tests 6 | # No need to traverse our git directory 7 | .git 8 | 9 | # Things to ignore: 10 | extend-ignore = 11 | # C101 - Coding magic comment not found 12 | C101, 13 | # C812 - missing trailing comma. Black figures it out. 14 | C812, 15 | # C815 - missing trailing comma in Python 3.5+. Black figures it out. 16 | C815, 17 | D, 18 | # E203 - Whitespace before ':'. Required by black. 19 | E203, 20 | # E501 - Line too long. Black will fold normal source lines. 21 | E501, 22 | # Q000 - Remove bad quotes. Black uses double quotes. 23 | Q000, 24 | # S305 - Use of insecure cipher mode cryptography.hazmat.primitives.ciphers.modes.ECB. 25 | S305, 26 | WPS, 27 | DAR, 28 | 29 | # __init__.py imports modules for library consumer use 30 | # F401 - Imported but unused 31 | per-file-ignores = __init__.py:F401 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020-2021 Konstantin Novichikhin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Upload coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: ['3.13'] 17 | steps: 18 | - name: Clone Repository (Latest) 19 | uses: actions/checkout@v4 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | python -m pip install -r requirements.txt 25 | python -m pip install pytest 26 | python -m pip install pytest-cov 27 | 28 | - name: Test with pytest 29 | run: | 30 | python -m pytest --cov --cov-report=xml 31 | 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v4 34 | with: 35 | files: ./coverage.xml 36 | flags: ubuntu-latest, ${{ matrix.python-version }} 37 | fail_ci_if_error: false 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint test clean coverage build publish 2 | 3 | # See setup.cfg for flake8 and mypy options 4 | lint: 5 | python -m black ./psec ./tests 6 | python -m flake8 7 | python -m mypy 8 | 9 | # See pyproject.toml for pytest options 10 | test: lint 11 | python -m pytest 12 | 13 | clean: 14 | $(MAKE) coverage-clean 15 | $(MAKE) build-clean 16 | 17 | # Generage and publish code coverage reports 18 | # See pyproject.toml coverage for options 19 | coverage-clean: 20 | rm --force --recursive .coverage 21 | rm --force --recursive ./htmlcov 22 | rm --force --recursive coverage.xml 23 | 24 | coverage: coverage-clean 25 | python -m pytest --cov=./psec 26 | python -m coverage html --directory ./htmlcov 27 | python -m coverage xml -o coverage.xml 28 | 29 | coverage-publish: coverage 30 | codecov -f coverage.xml -t $(TOKEN) 31 | 32 | # Build and upload release to PyPI 33 | build-clean: 34 | rm --force --recursive dist/ 35 | rm --force --recursive build/ 36 | rm --force --recursive *.egg-info 37 | 38 | build: build-clean 39 | python ./setup.py sdist bdist_wheel 40 | python -m twine check dist/* 41 | 42 | publish: build 43 | python -m twine upload dist/* 44 | 45 | publish-test: build 46 | python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* 47 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |pypi| |coverage| 2 | 3 | ``psec`` package provides tools for protecting sensitive data and 4 | cardholder authentication in retail payment transactions. 5 | 6 | Installation 7 | ------------ 8 | 9 | ``psec`` is published on `PyPI`__ and can be installed from there: 10 | 11 | .. code-block:: 12 | 13 | pip install psec 14 | 15 | __ https://pypi.org/project/psec/ 16 | 17 | Modules 18 | ------- 19 | 20 | - tr31 - TR-31 key block wrapping and unwrapping 21 | - cvv - Card Verification Value generation 22 | - des - Triple DES utilities (a wrapper over cryptography_) 23 | - aes - AES utilities (a wrapper over cryptography_) 24 | - mac - Message Authentication Code generation 25 | - pin - Personal Identification Number generation 26 | - pinblock - PIN Blocks encoding and decoding 27 | 28 | Contributors 29 | ------------ 30 | 31 | - `Konstantin Novichikhin `_ 32 | 33 | - Author 34 | 35 | - `David Schmid `_ 36 | 37 | - PIN block ISO 4 support 38 | 39 | .. _cryptography: https://pypi.org/project/cryptography/ 40 | 41 | .. |pypi| image:: https://img.shields.io/pypi/v/psec.svg 42 | :alt: PyPI 43 | :target: https://pypi.org/project/psec/ 44 | 45 | .. |coverage| image:: https://codecov.io/gh/knovichikhin/psec/branch/master/graph/badge.svg 46 | :alt: Test coverage 47 | :target: https://codecov.io/gh/knovichikhin/psec 48 | -------------------------------------------------------------------------------- /tests/test_cvv.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from psec import cvv 3 | 4 | 5 | # fmt: off 6 | @pytest.mark.parametrize( 7 | ["cvk", "pan", "expiry", "sc", "error"], 8 | [ 9 | (b"AAAAAAAA", "5555555551234567", "2012", "220", "CVK must be a double length DES key"), 10 | (b"AAAAAAAABBBBBBBBCCCCCCCC", "5555555551234567", "2012", "220", "CVK must be a double length DES key"), 11 | (b"AAAAAAAABBBBBBBB", "55555555512345671234", "2012", "220", "PAN must be less than 19 digits"), 12 | (b"AAAAAAAABBBBBBBB", "555555555123456A", "2012", "220", "PAN must be less than 19 digits"), 13 | (b"AAAAAAAABBBBBBBB", "5555555551234567", "201", "220", "PAN expiry must be 4 digits long"), 14 | (b"AAAAAAAABBBBBBBB", "5555555551234567", "20120", "220", "PAN expiry must be 4 digits long"), 15 | (b"AAAAAAAABBBBBBBB", "5555555551234567", "201A", "220", "PAN expiry must be 4 digits long"), 16 | (b"AAAAAAAABBBBBBBB", "5555555551234567", "2012", "2202", "Service code must be 3 digits long"), 17 | (b"AAAAAAAABBBBBBBB", "5555555551234567", "2012", "22", "Service code must be 3 digits long"), 18 | (b"AAAAAAAABBBBBBBB", "5555555551234567", "2012", "22A", "Service code must be 3 digits long"), 19 | ], 20 | ) 21 | # fmt: on 22 | def test_generate_cvv_exceptions( 23 | cvk: bytes, pan: str, expiry: str, sc: str, error: str 24 | ) -> None: 25 | with pytest.raises(ValueError, match=error): 26 | cvv.generate_cvv(cvk, pan, expiry, sc) 27 | 28 | 29 | def test_generate_cvv() -> None: 30 | cvk = "99999999999999998888888888888888" 31 | card_cvv = cvv.generate_cvv(bytes.fromhex(cvk), "2222222222222222", "3333", "111") 32 | assert card_cvv == "361" 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | classifiers = [ 4 | "Development Status :: 5 - Production/Stable", 5 | "Intended Audience :: Developers", 6 | "License :: OSI Approved :: MIT License", 7 | "Operating System :: OS Independent", 8 | "Programming Language :: Python", 9 | "Programming Language :: Python :: 3.8", 10 | "Programming Language :: Python :: 3.9", 11 | "Programming Language :: Python :: 3.10", 12 | "Programming Language :: Python :: 3.11", 13 | "Programming Language :: Python :: 3.12", 14 | "Programming Language :: Python :: 3.13", 15 | "Programming Language :: Python :: Implementation :: CPython", 16 | "Programming Language :: Python :: Implementation :: PyPy", 17 | "Topic :: Security :: Cryptography", 18 | "Topic :: Software Development :: Libraries :: Python Modules", 19 | ] 20 | 21 | if __name__ == "__main__": 22 | 23 | with open("README.rst", "r", encoding="utf-8") as f: 24 | readme = f.read() 25 | 26 | setup( 27 | name="psec", 28 | version="1.3.0", 29 | author="Konstantin Novichikhin", 30 | author_email="konstantin.novichikhin@gmail.com", 31 | description="A Python package for cryptography in payment systems", 32 | long_description=readme, 33 | long_description_content_type="text/x-rst", 34 | license="MIT", 35 | url="https://github.com/knovichikhin/psec", 36 | packages=find_packages(exclude=["tests"]), 37 | package_data={"psec": ["py.typed"]}, 38 | zip_safe=False, 39 | install_requires=[ 40 | "cryptography >= 1.0", 41 | ], 42 | classifiers=classifiers, 43 | python_requires=">=3.8", 44 | keywords="payment security cvv cvd cvc mac iso9797 tdes aes tr31", 45 | ) 46 | -------------------------------------------------------------------------------- /tests/test_aes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from psec import aes 3 | 4 | 5 | def test_encrypt_aes_cbc_exception() -> None: 6 | with pytest.raises(ValueError) as e: 7 | aes.encrypt_aes_cbc(b"1" * 16, b"0" * 16, b"") 8 | assert e.value.args[0] == "Data length (0) must be multiple of AES block size 16." 9 | 10 | with pytest.raises(ValueError) as e: 11 | aes.encrypt_aes_cbc(b"1" * 16, b"0" * 16, b"2" * 17) 12 | assert e.value.args[0] == "Data length (17) must be multiple of AES block size 16." 13 | 14 | 15 | def test_decrypt_aes_cbc_exception() -> None: 16 | with pytest.raises(ValueError) as e: 17 | aes.decrypt_aes_cbc(b"1" * 16, b"0" * 16, b"") 18 | assert e.value.args[0] == "Data length (0) must be multiple of AES block size 16." 19 | 20 | with pytest.raises(ValueError) as e: 21 | aes.decrypt_aes_cbc(b"1" * 16, b"0" * 16, b"2" * 17) 22 | assert e.value.args[0] == "Data length (17) must be multiple of AES block size 16." 23 | 24 | 25 | def test_encrypt_aes_ecb_exception() -> None: 26 | with pytest.raises(ValueError) as e: 27 | aes.encrypt_aes_ecb(b"1" * 16, b"") 28 | assert e.value.args[0] == "Data length (0) must be multiple of AES block size 16." 29 | 30 | with pytest.raises(ValueError) as e: 31 | aes.encrypt_aes_ecb(b"1" * 16, b"2" * 17) 32 | assert e.value.args[0] == "Data length (17) must be multiple of AES block size 16." 33 | 34 | 35 | def test_decrypt_aes_ecb_exception() -> None: 36 | with pytest.raises(ValueError) as e: 37 | aes.decrypt_aes_ecb(b"1" * 16, b"") 38 | assert e.value.args[0] == "Data length (0) must be multiple of AES block size 16." 39 | 40 | with pytest.raises(ValueError) as e: 41 | aes.decrypt_aes_ecb(b"1" * 16, b"2" * 17) 42 | assert e.value.args[0] == "Data length (17) must be multiple of AES block size 16." 43 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | inputs: 10 | git-ref: 11 | description: Git Ref (Optional) 12 | required: false 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | max-parallel: 4 19 | matrix: 20 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 21 | 'pypy3.9', 'pypy3.10'] 22 | steps: 23 | - name: Clone Repository (Latest) 24 | uses: actions/checkout@v4 25 | if: github.event.inputs.git-ref == '' 26 | 27 | - name: Clone Repository (Custom Ref) 28 | uses: actions/checkout@v4 29 | if: github.event.inputs.git-ref != '' 30 | with: 31 | ref: ${{ github.event.inputs.git-ref }} 32 | 33 | - name: Set up Python ${{ matrix.python-version }} 34 | uses: actions/setup-python@v5 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | 38 | - name: Install dependencies 39 | run: | 40 | python -m pip install --upgrade pip 41 | python -m pip install pytest 42 | python -m pip install -r requirements.txt 43 | 44 | - name: Test with pytest 45 | run: | 46 | python -m pytest --version 47 | python -m pytest 48 | 49 | - name: Test with mypy 50 | if: startsWith(matrix.python-version, 'pypy3') == false 51 | run: | 52 | python -m pip install mypy 53 | python -m pip install types-cryptography 54 | python -m mypy --version 55 | python -m mypy 56 | 57 | - name: Test with flake8 58 | run: | 59 | python -m pip install flake8 60 | python -m flake8 --version 61 | python -m flake8 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | pip-wheel-metadata/ 31 | share/python-wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | *.py,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | docs/_static/ 81 | docs/_templates/ 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # celery beat schedule file 104 | celerybeat-schedule 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | # VSCode 137 | .vscode.* 138 | .vscode/ 139 | -------------------------------------------------------------------------------- /psec/cvv.py: -------------------------------------------------------------------------------- 1 | import binascii as _binascii 2 | 3 | from psec import des as _des 4 | from psec import tools as _tools 5 | 6 | __all__ = ["generate_cvv"] 7 | 8 | 9 | def generate_cvv( 10 | cvk: bytes, 11 | pan: str, 12 | expiry: str, 13 | service_code: str, 14 | ) -> str: 15 | r"""Generate Visa/MasterCard Card Verification Value. 16 | 17 | Parameters 18 | ---------- 19 | cvk : bytes 20 | 16-byte binary card verification key. Has to be a valid Triple DES key. 21 | pan : str 22 | ASCII Primary Account Number. 23 | expiry : str 24 | ASCII PAN expiry date. It could be YYMM or MMYY depending on the issuer. 25 | service_code : str 26 | ASCII service code. 27 | 28 | Returns 29 | ------- 30 | cvv : str 31 | Card Verification Value. 32 | 33 | Raises 34 | ------ 35 | ValueError 36 | CVK must be a double length DES key 37 | PAN must be less than 19 digit 38 | PAN expiry must be 4 digits long 39 | Service code must be 3 digits long 40 | 41 | Notes 42 | ----- 43 | Both MasterCard and Visa use the same card verification algorithm. 44 | MasterCard calls the resulting value Card Verification Code (CVC). 45 | Visa calls the resulting value Card Verification Value (CVV). 46 | 47 | - CVV encoded on a magnetic stripe is called CVV1. CVV1 is 48 | encoded using informtation from the track1/2 itself. 49 | - CVV printed on the back of a card is called CVV2. CVV2 is 50 | encoded using informtation from the track1/2 itself 51 | while using a service code different from magnetic stripe (e.g. 000). 52 | - CVV encoded on an EMV track2 equivalent is called Chip CVC or iCVV. 53 | 'i' in iCVV stands for Integrated Card. iCVV is 54 | encoded using informtation from the EMV track1/2 itself 55 | while using a service code different from CVV1 and CVV2. 56 | Bottom line, CVV1, CVV2 and iCVV have to be different. 57 | 58 | Some cards employ dynamic CVV that changes with every transaction. 59 | Dynamic CVV is produced using a different algorithm. 60 | 61 | Examples 62 | -------- 63 | >>> import psec 64 | >>> cvk = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 65 | >>> psec.cvv.generate_cvv(cvk, "1234567890123456", "9912", "220") 66 | '170' 67 | """ 68 | 69 | if len(cvk) != 16: 70 | raise ValueError("CVK must be a double length DES key") 71 | 72 | if len(pan) > 19 or not _tools.ascii_numeric(pan): 73 | raise ValueError("PAN must be less than 19 digits") 74 | 75 | if len(expiry) != 4 or not _tools.ascii_numeric(expiry): 76 | raise ValueError("PAN expiry must be 4 digits long") 77 | 78 | if len(service_code) != 3 or not _tools.ascii_numeric(service_code): 79 | raise ValueError("Service code must be 3 digits long") 80 | 81 | block = (pan + expiry + service_code).ljust(32, "0") 82 | result = _des.encrypt_tdes_ecb(cvk[:8], _binascii.a2b_hex(block[:16])) 83 | result = _tools.xor(result, _binascii.a2b_hex(block[16:])) 84 | result = _des.encrypt_tdes_ecb(cvk, result) 85 | return "".join( 86 | [ 87 | c 88 | for c in result.hex() 89 | if c in {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"} 90 | ][:3] 91 | ) 92 | -------------------------------------------------------------------------------- /tests/test_des.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import psec 3 | 4 | 5 | # fmt: off 6 | @pytest.mark.parametrize( 7 | ["key", "variant", "error"], 8 | [ 9 | (b"AAAAAAA", 1, "Key must be a single, double or triple DES key"), 10 | (b"AAAAAAAABBBBBBBBCCCCCCCC", -1, "Variant must be in the range of 0 to 31"), 11 | (b"AAAAAAAABBBBBBBB", -1, "Variant must be in the range of 0 to 31"), 12 | (b"AAAAAAAA", 32, "Variant must be in the range of 0 to 31"), 13 | ], 14 | ) 15 | # fmt: on 16 | def test_key_variant_exceptions(key: bytes, variant: int, error: str) -> None: 17 | with pytest.raises(ValueError, match=error): 18 | psec.des.apply_key_variant(key, variant) 19 | 20 | 21 | # fmt: off 22 | @pytest.mark.parametrize( 23 | ["key", "variant", "key_variant"], 24 | [ 25 | ("0000000000000000", 1, "0800000000000000"), 26 | ("0000000000000000", 31, "F800000000000000"), 27 | ("00000000000000010000000000000002", 1, "08000000000000010800000000000002"), 28 | ("00000000000000010000000000000002", 31, "F800000000000001F800000000000002"), 29 | ("000000000000000100000000000000020000000000000003", 1, "080000000000000108000000000000020800000000000003"), 30 | ("000000000000000100000000000000020000000000000003", 31, "F800000000000001F800000000000002F800000000000003"), 31 | ], 32 | ) 33 | # fmt: on 34 | def test_key_variant(key: str, variant: int, key_variant: str) -> None: 35 | result = psec.des.apply_key_variant(bytes.fromhex(key), variant) 36 | assert result.hex().upper() == key_variant 37 | 38 | 39 | def test_encrypt_des_cbc_exception() -> None: 40 | with pytest.raises(ValueError) as e: 41 | psec.des.encrypt_tdes_cbc(b"1" * 16, b"0" * 16, b"") 42 | assert e.value.args[0] == "Data length (0) must be multiple of DES block size 8." 43 | 44 | with pytest.raises(ValueError) as e: 45 | psec.des.encrypt_tdes_cbc(b"1" * 16, b"0" * 16, b"2" * 17) 46 | assert e.value.args[0] == "Data length (17) must be multiple of DES block size 8." 47 | 48 | 49 | def test_decrypt_des_cbc_exception() -> None: 50 | with pytest.raises(ValueError) as e: 51 | psec.des.decrypt_tdes_cbc(b"1" * 16, b"0" * 16, b"") 52 | assert e.value.args[0] == "Data length (0) must be multiple of DES block size 8." 53 | 54 | with pytest.raises(ValueError) as e: 55 | psec.des.decrypt_tdes_cbc(b"1" * 16, b"0" * 16, b"2" * 17) 56 | assert e.value.args[0] == "Data length (17) must be multiple of DES block size 8." 57 | 58 | 59 | def test_encrypt_des_ecb_exception() -> None: 60 | with pytest.raises(ValueError) as e: 61 | psec.des.encrypt_tdes_ecb(b"1" * 16, b"") 62 | assert e.value.args[0] == "Data length (0) must be multiple of DES block size 8." 63 | 64 | with pytest.raises(ValueError) as e: 65 | psec.des.encrypt_tdes_ecb(b"1" * 16, b"2" * 17) 66 | assert e.value.args[0] == "Data length (17) must be multiple of DES block size 8." 67 | 68 | 69 | def test_decrypt_des_ecb_exception() -> None: 70 | with pytest.raises(ValueError) as e: 71 | psec.des.decrypt_tdes_ecb(b"1" * 16, b"") 72 | assert e.value.args[0] == "Data length (0) must be multiple of DES block size 8." 73 | 74 | with pytest.raises(ValueError) as e: 75 | psec.des.decrypt_tdes_ecb(b"1" * 16, b"2" * 17) 76 | assert e.value.args[0] == "Data length (17) must be multiple of DES block size 8." 77 | -------------------------------------------------------------------------------- /psec/tools.py: -------------------------------------------------------------------------------- 1 | import sys as _sys 2 | 3 | __all__ = [ 4 | "xor", 5 | "odd_parity", 6 | "ascii_alphanumeric", 7 | "ascii_numeric", 8 | "ascii_printable", 9 | "ascii_hexchar", 10 | ] 11 | 12 | 13 | def xor(data: bytes, key: bytes) -> bytes: 14 | r"""Apply "exlusive or" to two bytes instances. 15 | Many thanks: 16 | https://stackoverflow.com/a/29409299 17 | 18 | Parameters 19 | ---------- 20 | data : bytes 21 | Data to be XOR'd 22 | key : bytes 23 | Bit mask used to XOR data 24 | 25 | Returns 26 | ------- 27 | bytes 28 | Data XOR'd by key 29 | """ 30 | key = key[: len(data)] 31 | int_var = int.from_bytes(data, _sys.byteorder) 32 | int_key = int.from_bytes(key, _sys.byteorder) 33 | int_enc = int_var ^ int_key 34 | return int_enc.to_bytes(len(data), _sys.byteorder) 35 | 36 | 37 | def odd_parity(v: int) -> int: 38 | r"""Check integer parity. 39 | Many thanks: in_parallel 40 | http://p-nand-q.com/python/_algorithms/math/bit-parity.html 41 | 42 | Parameters 43 | ---------- 44 | v : int 45 | Integer to check parity of 46 | 47 | Returns 48 | ------- 49 | int 50 | 0 = even parity (even number of bits enabled, e.g. 0, 3, 5) 51 | 1 = odd parity (odd number of bits enabled, e.g. 1, 2, 4) 52 | """ 53 | v ^= v >> 16 54 | v ^= v >> 8 55 | v ^= v >> 4 56 | v &= 0xF 57 | return (0x6996 >> v) & 1 58 | 59 | 60 | _ascii_n = frozenset("0123456789") 61 | _ascii_an = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 62 | _ascii_pa = frozenset( 63 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 " 64 | + r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""" 65 | ) 66 | _ascii_h = frozenset("abcdefABCDEF0123456789") 67 | 68 | 69 | def ascii_alphanumeric(s: str) -> bool: 70 | r"""Check if string is ASCII alphanumeric (A-Z, a-z, 0-9). 71 | 72 | Parameters 73 | ---------- 74 | s : str 75 | String to check. 76 | 77 | Returns 78 | ------- 79 | bool 80 | True if string is ASCII alphanumeric. False, otherwise. 81 | """ 82 | return frozenset(s).issubset(_ascii_an) 83 | 84 | 85 | def ascii_numeric(s: str) -> bool: 86 | r"""Check if string is ASCII numeric (0-9). 87 | 88 | Parameters 89 | ---------- 90 | s : str 91 | String to check. 92 | 93 | Returns 94 | ------- 95 | bool 96 | True if string is ASCII numeric. False, otherwise. 97 | """ 98 | return frozenset(s).issubset(_ascii_n) 99 | 100 | 101 | def ascii_printable(s: str) -> bool: 102 | r"""Check if string is ASCII printable. 103 | Printable ASCII characters are those with hex values 104 | in the range 20-7E, inclusive. 105 | 106 | Parameters 107 | ---------- 108 | s : str 109 | String to check. 110 | 111 | Returns 112 | ------- 113 | bool 114 | True if string is ASCII printable. False, otherwise. 115 | """ 116 | return frozenset(s).issubset(_ascii_pa) 117 | 118 | 119 | def ascii_hexchar(s: str) -> bool: 120 | r"""Check if string is ASCII hexchar (A-F, a-f, 0-9). 121 | 122 | Parameters 123 | ---------- 124 | s : str 125 | String to check. 126 | 127 | Returns 128 | ------- 129 | bool 130 | True if string is ASCII hexchar. False, otherwise. 131 | """ 132 | return frozenset(s).issubset(_ascii_h) 133 | -------------------------------------------------------------------------------- /tests/test_mac.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import pytest 3 | from psec import mac 4 | 5 | 6 | @pytest.mark.parametrize( 7 | ["data", "block_len", "result"], 8 | [ 9 | ("1234", 8, "1234000000000000"), 10 | ("1234567890123456", 8, "1234567890123456"), 11 | ("1234", None, "1234000000000000"), 12 | ("1234567890123456", None, "1234567890123456"), 13 | ("1234", 4, "12340000"), 14 | ("12345678", 4, "12345678"), 15 | ("", 4, "00000000"), 16 | ], 17 | ) 18 | def test_pad_iso_1(data: str, block_len: Optional[int], result: str) -> None: 19 | assert result == mac.pad_iso_1(bytes.fromhex(data), block_len).hex() 20 | 21 | 22 | @pytest.mark.parametrize( 23 | ["data", "block_len", "result"], 24 | [ 25 | ("1234", 8, "1234800000000000"), 26 | ("12345678901234", 8, "1234567890123480"), 27 | ("1234567890123456", 8, "12345678901234568000000000000000"), 28 | ("1234", None, "1234800000000000"), 29 | ("12345678901234", None, "1234567890123480"), 30 | ("1234567890123456", None, "12345678901234568000000000000000"), 31 | ("1234", 4, "12348000"), 32 | ("123456", 4, "12345680"), 33 | ("12345678", 4, "1234567880000000"), 34 | ("", 4, "80000000"), 35 | ], 36 | ) 37 | def test_pad_iso_2(data: str, block_len: Optional[int], result: str) -> None: 38 | assert result == mac.pad_iso_2(bytes.fromhex(data), block_len).hex() 39 | 40 | 41 | @pytest.mark.parametrize( 42 | ["data", "block_len", "result"], 43 | [ 44 | ("1234", 8, "00000000000000101234000000000000"), 45 | ("1234567890123456", 8, "00000000000000401234567890123456"), 46 | ("1234", None, "00000000000000101234000000000000"), 47 | ("1234567890123456", None, "00000000000000401234567890123456"), 48 | ("1234", 4, "0000001012340000"), 49 | ("12345678", 4, "0000002012345678"), 50 | ("", 4, "0000000000000000"), 51 | ], 52 | ) 53 | def test_pad_iso_3(data: str, block_len: Optional[int], result: str) -> None: 54 | assert result == mac.pad_iso_3(bytes.fromhex(data), block_len).hex() 55 | 56 | 57 | def test_generate_retail_mac_exception() -> None: 58 | with pytest.raises( 59 | ValueError, 60 | match="Specify valid padding method: 1, 2 or 3.", 61 | ): 62 | mac.generate_retail_mac( 63 | b"AAAAAAAAAAAAAAAA", 64 | b"BBBBBBBBBBBBBBBB", 65 | b"hello world", 66 | 4, 67 | ) 68 | 69 | 70 | @pytest.mark.parametrize( 71 | ["padding", "length", "result"], 72 | [ 73 | (1, 8, "3DDE5C5511661CBF"), 74 | (2, 8, "E16941032C3BC7D4"), 75 | (3, 8, "BA90F750EF43F668"), 76 | (1, None, "3DDE5C5511661CBF"), 77 | (2, None, "E16941032C3BC7D4"), 78 | (3, None, "BA90F750EF43F668"), 79 | (1, 4, "3DDE5C55"), 80 | (2, 4, "E1694103"), 81 | (3, 4, "BA90F750"), 82 | ], 83 | ) 84 | def test_generate_retail_mac(padding: int, length: Optional[int], result: str) -> None: 85 | assert result == ( 86 | mac.generate_retail_mac( 87 | b"AAAAAAAAAAAAAAAA", b"BBBBBBBBBBBBBBBB", b"hello world", padding, length 88 | ) 89 | .hex() 90 | .upper() 91 | ) 92 | 93 | 94 | def test_generate_cbc_mac_exception() -> None: 95 | with pytest.raises( 96 | ValueError, 97 | match="Specify valid padding method: 1, 2 or 3.", 98 | ): 99 | mac.generate_cbc_mac( 100 | b"AAAAAAAAAAAAAAAA", 101 | b"hello world", 102 | 4, 103 | ) 104 | 105 | 106 | @pytest.mark.parametrize( 107 | ["padding", "length", "result"], 108 | [ 109 | (1, 8, "68D9038F23360DF3"), 110 | (2, 8, "32DC341271ACCD00"), 111 | (3, 8, "CDACA53E2DAA5412"), 112 | (1, None, "68D9038F23360DF3"), 113 | (2, None, "32DC341271ACCD00"), 114 | (3, None, "CDACA53E2DAA5412"), 115 | (1, 4, "68D9038F"), 116 | (2, 4, "32DC3412"), 117 | (3, 4, "CDACA53E"), 118 | ], 119 | ) 120 | def test_generate_cbc_mac(padding: int, length: Optional[int], result: str) -> None: 121 | assert result == ( 122 | mac.generate_cbc_mac( 123 | bytes.fromhex("AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB"), 124 | b"hello world", 125 | padding, 126 | length, 127 | ) 128 | .hex() 129 | .upper() 130 | ) 131 | -------------------------------------------------------------------------------- /psec/aes.py: -------------------------------------------------------------------------------- 1 | from cryptography.hazmat.backends import default_backend as _default_backend 2 | from cryptography.hazmat.primitives.ciphers import Cipher as _Cipher 3 | from cryptography.hazmat.primitives.ciphers import algorithms as _algorithms 4 | from cryptography.hazmat.primitives.ciphers import modes as _modes 5 | 6 | __all__ = [ 7 | "encrypt_aes_cbc", 8 | "encrypt_aes_ecb", 9 | "decrypt_aes_cbc", 10 | "decrypt_aes_ecb", 11 | ] 12 | 13 | 14 | def encrypt_aes_cbc(key: bytes, iv: bytes, data: bytes) -> bytes: 15 | r"""Encrypt data using AES CBC algorithm. 16 | 17 | Parameters 18 | ---------- 19 | key : bytes 20 | Binary AES key. 21 | iv : bytes 22 | Binary initial initialization vector for CBC. 23 | Has to be 16 bytes long. 24 | data : bytes 25 | Binary data to be encrypted. 26 | Has to be multiple of 16 bytes. 27 | 28 | Returns 29 | ------- 30 | encrypted_data : bytes 31 | Binary encrypted data. 32 | 33 | Raises 34 | ------ 35 | ValueError 36 | Data length must be multiple of AES block size 16. 37 | 38 | Examples 39 | -------- 40 | >>> import psec 41 | >>> key = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 42 | >>> iv = bytes.fromhex("00000000000000000000000000000000") 43 | >>> data = bytes.fromhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") * 2 44 | >>> psec.aes.encrypt_aes_cbc(key, iv, data).hex().upper() 45 | '592373540AE1B202615E6D210D868A8C6593A91B63F201B28860C4DE39375EB4' 46 | """ 47 | if len(data) < 16 or len(data) % 16 != 0: 48 | raise ValueError( 49 | f"Data length ({str(len(data))}) must be multiple of AES block size 16." 50 | ) 51 | 52 | cipher = _Cipher( 53 | _algorithms.AES(key), 54 | _modes.CBC(iv), 55 | backend=_default_backend(), 56 | ) 57 | return cipher.encryptor().update(data) 58 | 59 | 60 | def encrypt_aes_ecb(key: bytes, data: bytes) -> bytes: 61 | r"""Encrypt data using AES ECB algorithm. 62 | 63 | Parameters 64 | ---------- 65 | key : bytes 66 | Binary AES key. 67 | data : bytes 68 | Binary data to be encrypted. 69 | Has to be multiple of 16 bytes. 70 | 71 | Returns 72 | ------- 73 | encrypted_data : bytes 74 | Binary encrypted data. 75 | 76 | Raises 77 | ------ 78 | ValueError 79 | Data length must be multiple of AES block size 16. 80 | 81 | Examples 82 | -------- 83 | >>> import psec 84 | >>> key = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 85 | >>> iv = bytes.fromhex("00000000000000000000000000000000") 86 | >>> data = bytes.fromhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") * 2 87 | >>> psec.aes.encrypt_aes_ecb(key, data).hex().upper() 88 | '592373540AE1B202615E6D210D868A8C592373540AE1B202615E6D210D868A8C' 89 | """ 90 | if len(data) < 16 or len(data) % 16 != 0: 91 | raise ValueError( 92 | f"Data length ({str(len(data))}) must be multiple of AES block size 16." 93 | ) 94 | 95 | cipher = _Cipher(_algorithms.AES(key), _modes.ECB(), backend=_default_backend()) 96 | return cipher.encryptor().update(data) 97 | 98 | 99 | def decrypt_aes_cbc(key: bytes, iv: bytes, data: bytes) -> bytes: 100 | r"""Decrypt data using AES CBC algorithm. 101 | 102 | Parameters 103 | ---------- 104 | key : bytes 105 | Binary AES key. 106 | iv : bytes 107 | Binary initial initialization vector for CBC. 108 | Has to be 16 bytes long. 109 | data : bytes 110 | Binary data to be decrypted. 111 | Has to be multiple of 16 bytes. 112 | 113 | Returns 114 | ------- 115 | decrypted_data : bytes 116 | Binary decrypted data. 117 | 118 | Raises 119 | ------ 120 | ValueError 121 | Data length must be multiple of AES block size 16. 122 | 123 | Examples 124 | -------- 125 | >>> import psec 126 | >>> key = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 127 | >>> iv = bytes.fromhex("00000000000000000000000000000000") 128 | >>> cipher_text = bytes.fromhex("592373540AE1B202615E6D210D868A8C6593A91B63F201B28860C4DE39375EB4") 129 | >>> psec.aes.decrypt_aes_cbc(key, iv, cipher_text).hex().upper() 130 | 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' 131 | """ 132 | if len(data) < 16 or len(data) % 16 != 0: 133 | raise ValueError( 134 | f"Data length ({str(len(data))}) must be multiple of AES block size 16." 135 | ) 136 | 137 | cipher = _Cipher( 138 | _algorithms.AES(key), 139 | _modes.CBC(iv), 140 | backend=_default_backend(), 141 | ) 142 | return cipher.decryptor().update(data) 143 | 144 | 145 | def decrypt_aes_ecb(key: bytes, data: bytes) -> bytes: 146 | r"""Decrypt data using AES ECB algorithm. 147 | 148 | Parameters 149 | ---------- 150 | key : bytes 151 | Binary AES key. 152 | data : bytes 153 | Binary data to be decrypted. 154 | Has to be multiple of 16 bytes. 155 | 156 | Returns 157 | ------- 158 | decrypted_data : bytes 159 | Binary decrypted data. 160 | 161 | Raises 162 | ------ 163 | ValueError 164 | Data length must be multiple of AES block size 16. 165 | 166 | Examples 167 | -------- 168 | >>> import psec 169 | >>> key = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 170 | >>> cipher_text = bytes.fromhex("592373540AE1B202615E6D210D868A8C592373540AE1B202615E6D210D868A8C") 171 | >>> psec.aes.decrypt_aes_ecb(key, cipher_text).hex().upper() 172 | 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' 173 | """ 174 | if len(data) < 16 or len(data) % 16 != 0: 175 | raise ValueError( 176 | f"Data length ({str(len(data))}) must be multiple of AES block size 16." 177 | ) 178 | 179 | cipher = _Cipher(_algorithms.AES(key), _modes.ECB(), backend=_default_backend()) 180 | return cipher.decryptor().update(data) 181 | -------------------------------------------------------------------------------- /psec/des.py: -------------------------------------------------------------------------------- 1 | import typing as _typing 2 | 3 | from cryptography.hazmat.backends import default_backend as _default_backend 4 | from cryptography.hazmat.primitives.ciphers import Cipher as _Cipher 5 | from cryptography.hazmat.primitives.ciphers import algorithms as _algorithms 6 | from cryptography.hazmat.primitives.ciphers import modes as _modes 7 | 8 | from psec import tools as _tools 9 | 10 | __all__ = [ 11 | "apply_key_variant", 12 | "adjust_key_parity", 13 | "generate_kcv", 14 | "encrypt_tdes_cbc", 15 | "encrypt_tdes_ecb", 16 | "decrypt_tdes_cbc", 17 | "decrypt_tdes_ecb", 18 | ] 19 | 20 | 21 | def apply_key_variant(key: _typing.Union[bytes, bytearray], variant: int) -> bytes: 22 | r"""Apply variant to the most significant byte of each DES key pair. 23 | 24 | Parameters 25 | ---------- 26 | key : bytes 27 | Binary (Triple) DES key. Has to be a valid DES key. 28 | variant : bytes 29 | Variant in the range of 0 and 31. 30 | 31 | Returns 32 | ------- 33 | key_variant : bytes 34 | Binary key under desired variant. 35 | 36 | Raises 37 | ------ 38 | ValueError 39 | Key must be a single, double or triple DES key 40 | Variant must be in the range of 0 to 31 41 | 42 | Examples 43 | -------- 44 | >>> import psec 45 | >>> key = bytes.fromhex("0123456789ABCDEF") 46 | >>> psec.des.apply_key_variant(key, 1).hex().upper() 47 | '0923456789ABCDEF' 48 | """ 49 | 50 | if len(key) not in {8, 16, 24}: 51 | raise ValueError("Key must be a single, double or triple DES key") 52 | 53 | if variant < 0 or variant > 31: 54 | raise ValueError("Variant must be in the range of 0 to 31") 55 | 56 | mask = ((8 * variant).to_bytes(1, "big") + (b"\x00" * 7)) * (len(key) // 8) 57 | return _tools.xor(key, mask) 58 | 59 | 60 | def adjust_key_parity(key: _typing.Union[bytes, bytearray]) -> bytes: 61 | r"""Adjust DES key parity key 62 | 63 | Parameters 64 | ---------- 65 | key : bytes, bytearray 66 | Binary key to adjust for odd parity. 67 | 68 | Returns 69 | ------- 70 | adjusted_key : bytes 71 | Binary key adjusted for odd parity. 72 | 73 | Examples 74 | -------- 75 | >>> import psec 76 | >>> key = bytes.fromhex("1A2B3C4D5F0A1B2C4D5F6A7B8C9D0F1A") 77 | >>> psec.des.adjust_key_parity(key).hex().upper() 78 | '1A2A3D4C5E0B1A2C4C5E6B7A8C9D0E1A' 79 | """ 80 | adjusted_key = bytearray(key) 81 | 82 | for i, byte in enumerate(adjusted_key): 83 | if not _tools.odd_parity(byte): 84 | adjusted_key[i] ^= 1 85 | 86 | return bytes(adjusted_key) 87 | 88 | 89 | def generate_kcv(key: bytes, length: int = 2) -> bytes: 90 | r"""Generate DES key checksum value (KCV). 91 | 92 | Parameters 93 | ---------- 94 | key : bytes 95 | Binary key to provide check digits for. Has to be a valid DES key. 96 | length : int, optional 97 | Number of KCV bytes returned (default 2). 98 | 99 | Returns 100 | ------- 101 | kcv : bytes 102 | Binary KCV (`length` bytes) 103 | 104 | Examples 105 | -------- 106 | >>> import psec 107 | >>> key = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 108 | >>> psec.des.generate_kcv(key).hex().upper() 109 | '08D7' 110 | """ 111 | cipher = _Cipher( 112 | _algorithms.TripleDES(key), _modes.ECB(), backend=_default_backend() 113 | ) 114 | encryptor = cipher.encryptor() 115 | return encryptor.update(b"\x00\x00\x00\x00\x00\x00\x00\x00")[:length] 116 | 117 | 118 | def encrypt_tdes_cbc(key: bytes, iv: bytes, data: bytes) -> bytes: 119 | r"""Encrypt data using Triple DES CBC algorithm. 120 | 121 | Parameters 122 | ---------- 123 | key : bytes 124 | Binary Triple DES key. Has to be a valid DES key. 125 | iv : bytes 126 | Binary initial initialization vector for CBC. 127 | Has to be 8 bytes long. 128 | data : bytes 129 | Binary data to be encrypted. 130 | Has to be multiple of 8 bytes. 131 | 132 | Returns 133 | ------- 134 | encrypted_data : bytes 135 | Binary encrypted data. 136 | 137 | Raises 138 | ------ 139 | ValueError 140 | Data length must be multiple of DES block size 8. 141 | 142 | Examples 143 | -------- 144 | >>> import psec 145 | >>> key = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 146 | >>> iv = bytes.fromhex("0000000000000000") 147 | >>> psec.des.encrypt_tdes_cbc(key, iv, b"12345678").hex().upper() 148 | '41D2FFBA3CDC15FE' 149 | """ 150 | if len(data) < 8 or len(data) % 8 != 0: 151 | raise ValueError( 152 | f"Data length ({str(len(data))}) must be multiple of DES block size 8." 153 | ) 154 | 155 | cipher = _Cipher( 156 | _algorithms.TripleDES(key), 157 | _modes.CBC(iv), 158 | backend=_default_backend(), 159 | ) 160 | return cipher.encryptor().update(data) 161 | 162 | 163 | def encrypt_tdes_ecb(key: bytes, data: bytes) -> bytes: 164 | r"""Encrypt data using Triple DES ECB algorithm. 165 | 166 | Parameters 167 | ---------- 168 | key : bytes 169 | Binary Triple DES key. Has to be a valid DES key. 170 | data : bytes 171 | Binary data to be encrypted. 172 | Has to be multiple of 8 bytes. 173 | 174 | Returns 175 | ------- 176 | encrypted_data : bytes 177 | Binary encrypted data. 178 | 179 | Raises 180 | ------ 181 | ValueError 182 | Data length must be multiple of DES block size 8. 183 | 184 | Examples 185 | -------- 186 | >>> import psec 187 | >>> key = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 188 | >>> psec.des.encrypt_tdes_ecb(key, b"12345678").hex().upper() 189 | '41D2FFBA3CDC15FE' 190 | """ 191 | if len(data) < 8 or len(data) % 8 != 0: 192 | raise ValueError( 193 | f"Data length ({str(len(data))}) must be multiple of DES block size 8." 194 | ) 195 | 196 | cipher = _Cipher( 197 | _algorithms.TripleDES(key), _modes.ECB(), backend=_default_backend() 198 | ) 199 | return cipher.encryptor().update(data) 200 | 201 | 202 | def decrypt_tdes_cbc(key: bytes, iv: bytes, data: bytes) -> bytes: 203 | r"""Decrypt data using Triple DES CBC algorithm. 204 | 205 | Parameters 206 | ---------- 207 | key : bytes 208 | Binary Triple DES key. Has to be a valid DES key. 209 | iv : bytes 210 | Binary initial initialization vector for CBC. 211 | Has to be 8 bytes long. 212 | data : bytes 213 | Binary data to be decrypted. 214 | Has to be multiple of 8 bytes. 215 | 216 | Returns 217 | ------- 218 | decrypted_data : bytes 219 | Binary decrypted data. 220 | 221 | Raises 222 | ------ 223 | ValueError 224 | Data length must be multiple of DES block size 8. 225 | 226 | Examples 227 | -------- 228 | >>> import psec 229 | >>> key = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 230 | >>> iv = bytes.fromhex("0000000000000000") 231 | >>> psec.des.decrypt_tdes_cbc(key, iv, bytes.fromhex("41D2FFBA3CDC15FE")) 232 | b'12345678' 233 | """ 234 | if len(data) < 8 or len(data) % 8 != 0: 235 | raise ValueError( 236 | f"Data length ({str(len(data))}) must be multiple of DES block size 8." 237 | ) 238 | 239 | cipher = _Cipher( 240 | _algorithms.TripleDES(key), 241 | _modes.CBC(iv), 242 | backend=_default_backend(), 243 | ) 244 | return cipher.decryptor().update(data) 245 | 246 | 247 | def decrypt_tdes_ecb(key: bytes, data: bytes) -> bytes: 248 | r"""Decrypt data using Triple DES ECB algorithm. 249 | 250 | Parameters 251 | ---------- 252 | key : bytes 253 | Binary Triple DES key. Has to be a valid DES key. 254 | data : bytes 255 | Binary data to be decrypted. 256 | Has to be multiple of 8 bytes. 257 | 258 | Returns 259 | ------- 260 | decrypted_data : bytes 261 | Binary decrypted data. 262 | 263 | Raises 264 | ------ 265 | ValueError 266 | Data length must be multiple of DES block size 8. 267 | 268 | Examples 269 | -------- 270 | >>> import psec 271 | >>> key = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 272 | >>> psec.des.decrypt_tdes_ecb(key, bytes.fromhex("41D2FFBA3CDC15FE")) 273 | b'12345678' 274 | """ 275 | if len(data) < 8 or len(data) % 8 != 0: 276 | raise ValueError( 277 | f"Data length ({str(len(data))}) must be multiple of DES block size 8." 278 | ) 279 | 280 | cipher = _Cipher( 281 | _algorithms.TripleDES(key), _modes.ECB(), backend=_default_backend() 282 | ) 283 | return cipher.decryptor().update(data) 284 | -------------------------------------------------------------------------------- /psec/pin.py: -------------------------------------------------------------------------------- 1 | from psec import des as _des 2 | from psec import tools as _tools 3 | 4 | __all__ = [ 5 | "generate_ibm3624_pin", 6 | "generate_ibm3624_offset", 7 | "generate_visa_pvv", 8 | ] 9 | 10 | 11 | def generate_ibm3624_pin( 12 | pvk: bytes, 13 | conversion_table: str, 14 | offset: str, 15 | pan: str, 16 | pan_verify_offset: int, 17 | pan_verify_length: int, 18 | pan_pad: str, 19 | ) -> str: 20 | r"""Generate IBM 3624 PIN based on PAN. 21 | 22 | Parameters 23 | ---------- 24 | pvk : bytes 25 | Binary PIN verification key. Has to be a valid Triple DES key. 26 | conversion_table : str 27 | Conversion table to map hexadecimal digits to decimal digits. 28 | This field contains 16 decimal digits. 29 | offset : str 30 | Offset applied to the generated (natural) PIN. 31 | Has to be 4-16 digits. Length of offset determines PIN length. 32 | Provide offset of all zeros to generate natural PIN. 33 | pan : str 34 | Primary Account Number to serve as validation data 35 | pan_verify_offset : int 36 | Offset in PAN to start validation data 37 | pan_verify_length : int 38 | Lenght of PAN to include into validation data 39 | pan_pad : str 40 | Character to pad validation data if not 16 characters long. 41 | Has to be a valid hex character. 42 | 43 | Returns 44 | ------- 45 | pin : str 46 | Cardholder Personal Identification Number 47 | 48 | Raises 49 | ------ 50 | ValueError 51 | PVK must be a DES key 52 | Conversion table must 16 digits 53 | Offset must be from 4 to 16 digits 54 | PAN must be less than 19 digit 55 | PAN pad character must be valid hex digit 56 | PAN verify offset and length must be within provided PAN 57 | 58 | Examples 59 | -------- 60 | >>> import psec 61 | >>> pvk = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 62 | >>> psec.pin.generate_ibm3624_pin( 63 | ... pvk, 64 | ... conversion_table="1234567890123456", 65 | ... offset="0000", 66 | ... pan="1122334455667788", 67 | ... pan_verify_offset=0, 68 | ... pan_verify_length=16, 69 | ... pan_pad="F") 70 | '4524' 71 | """ 72 | if len(pvk) not in {8, 16, 24}: 73 | raise ValueError("PVK must be a DES key") 74 | 75 | if len(conversion_table) != 16 or not _tools.ascii_numeric(conversion_table): 76 | raise ValueError("Conversion table must 16 digits") 77 | 78 | if len(offset) < 4 or len(offset) > 16 or not _tools.ascii_numeric(offset): 79 | raise ValueError("Offset must be from 4 to 16 digits") 80 | 81 | if len(pan) > 19 or not _tools.ascii_numeric(pan): 82 | raise ValueError("PAN must be less than 19 digits") 83 | 84 | if len(pan_pad) != 1 or not _tools.ascii_hexchar(pan_pad): 85 | raise ValueError("PAN pad character must be valid hex digit") 86 | 87 | validation_data = pan[pan_verify_offset : pan_verify_length + pan_verify_offset] 88 | 89 | if len(validation_data) != pan_verify_length: 90 | raise ValueError("PAN verify offset and length must be within provided PAN") 91 | 92 | validation_data = validation_data[:16].ljust(16, pan_pad[:1]).upper() 93 | 94 | intermediate_pin = ( 95 | _des.encrypt_tdes_ecb(pvk, bytes.fromhex(validation_data)).hex().upper() 96 | ) 97 | intermediate_pin = str.translate( 98 | intermediate_pin, str.maketrans("0123456789ABCDEF", conversion_table) 99 | ) 100 | 101 | return "".join( 102 | str(int(intermediate_pin[i]) + int(offset[i]))[-1:] 103 | for i in range(0, len(offset)) 104 | ) 105 | 106 | 107 | def generate_ibm3624_offset( 108 | pvk: bytes, 109 | conversion_table: str, 110 | pin: str, 111 | pan: str, 112 | pan_verify_offset: int, 113 | pan_verify_length: int, 114 | pan_pad: str, 115 | ) -> str: 116 | r"""Generate IBM 3624 PIN based on PAN. 117 | 118 | Parameters 119 | ---------- 120 | pvk : bytes 121 | Binary PIN verification key. Has to be a valid Triple DES key. 122 | conversion_table : str 123 | Conversion table to map hexadecimal digits to decimal digits. 124 | This field contains 16 decimal digits. 125 | pin : str 126 | Cardholder Personal Identification Number 127 | pan : str 128 | Primary Account Number to serve as validation data 129 | pan_verify_offset : int 130 | Offset in PAN to start validation data 131 | pan_verify_length : int 132 | Lenght of PAN to include into validation data 133 | pan_pad : str 134 | Character to pad validation data if not 16 characters long. 135 | Has to be a valid hex character. 136 | 137 | Returns 138 | ------- 139 | offset : str 140 | Offset applied to the generated (natural) PIN to arrive 141 | at cardholder PIN. 142 | 143 | Raises 144 | ------ 145 | ValueError 146 | PVK must be a DES key 147 | Conversion table must 16 digits 148 | PIN must be from 4 to 16 digits 149 | PAN must be less than 19 digit 150 | PAN pad character must be valid hex digit 151 | PAN verify offset and length must be within provided PAN 152 | 153 | Examples 154 | -------- 155 | >>> import psec 156 | >>> pvk = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 157 | >>> psec.pin.generate_ibm3624_offset( 158 | ... pvk, 159 | ... conversion_table="1234567890123456", 160 | ... pin="4524", 161 | ... pan="1122334455667788", 162 | ... pan_verify_offset=0, 163 | ... pan_verify_length=16, 164 | ... pan_pad="F") 165 | '0000' 166 | """ 167 | if len(pvk) not in {8, 16, 24}: 168 | raise ValueError("PVK must be a DES key") 169 | 170 | if len(conversion_table) != 16 or not _tools.ascii_numeric(conversion_table): 171 | raise ValueError("Conversion table must 16 digits") 172 | 173 | if len(pin) < 4 or len(pin) > 16 or not _tools.ascii_numeric(pin): 174 | raise ValueError("PIN must be from 4 to 16 digits") 175 | 176 | if len(pan) > 19 or not _tools.ascii_numeric(pan): 177 | raise ValueError("PAN must be less than 19 digits") 178 | 179 | if len(pan_pad) != 1 or not _tools.ascii_hexchar(pan_pad): 180 | raise ValueError("PAN pad character must be valid hex digit") 181 | 182 | validation_data = pan[pan_verify_offset : pan_verify_length + pan_verify_offset] 183 | 184 | if len(validation_data) != pan_verify_length: 185 | raise ValueError("PAN verify offset and length must be within provided PAN") 186 | 187 | validation_data = validation_data[:16].ljust(16, pan_pad[:1]).upper() 188 | 189 | intermediate_pin = ( 190 | _des.encrypt_tdes_ecb(pvk, bytes.fromhex(validation_data)).hex().upper() 191 | ) 192 | intermediate_pin = str.translate( 193 | intermediate_pin, str.maketrans("0123456789ABCDEF", conversion_table) 194 | ) 195 | 196 | return "".join( 197 | str(10 + int(pin[i]) - int(intermediate_pin[i]))[-1:] 198 | for i in range(0, len(pin)) 199 | ) 200 | 201 | 202 | def generate_visa_pvv( 203 | pvk: bytes, 204 | pvki: str, 205 | pin: str, 206 | pan: str, 207 | ) -> str: 208 | r"""Generate Visa PIN Verification Value. 209 | 210 | Parameters 211 | ---------- 212 | pvk : bytes 213 | Binary PIN Verification Key. Has to be a valid Triple DES key. 214 | pvki : str 215 | PIN Verification Key Index used in the algorithm to calculate. 216 | Contains 1 decimal digit in the range from "0" to "9". 217 | pin : str 218 | Cardholder Personal Identification Number 219 | pan : str 220 | Primary Account Number. 221 | 222 | Returns 223 | ------- 224 | pvv : str 225 | 4-digit PIN Verification Value. 226 | 227 | Raises 228 | ------ 229 | ValueError 230 | PVK must be a DES key 231 | PVKI must be 1 digit from "0" to "9" 232 | PIN must be 4 digits 233 | PAN must be more than 12 digits 234 | 235 | Examples 236 | -------- 237 | >>> import psec 238 | >>> pvk = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 239 | >>> psec.pin.generate_visa_pvv( 240 | ... pvk, 241 | ... pvki="3", 242 | ... pin="4524", 243 | ... pan="1122334455667788") 244 | '4021' 245 | """ 246 | 247 | if len(pvk) not in {8, 16, 24}: 248 | raise ValueError("PVK must be a DES key") 249 | 250 | if len(pvki) != 1 or not _tools.ascii_numeric(pvki): 251 | raise ValueError('PVKI must be 1 digit from "0" to "9"') 252 | 253 | if len(pin) != 4 or not _tools.ascii_numeric(pin): 254 | raise ValueError("PIN must be 4 digits") 255 | 256 | if len(pan) < 12 or not _tools.ascii_numeric(pan): 257 | raise ValueError("PAN must be more than 12 digits") 258 | 259 | # Form a "Transformed Security Parameter" 260 | tsp = pan[-12:-1] + pvki + pin 261 | tsp = _des.encrypt_tdes_ecb(pvk, bytes.fromhex(tsp)).hex() 262 | 263 | # 4 digits from TSP form a PVV 264 | pvv = "".join( 265 | [c for c in tsp if c in {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"}][:4] 266 | ) 267 | 268 | # If there are not enough digits, substitute letters in TSP with digits: 269 | # Input a b c d e f 270 | # Output 0 1 2 3 4 5 271 | if len(pvv) < 4: 272 | pvv2 = "".join( 273 | [c for c in tsp if c in {"a", "b", "c", "d", "e", "f"}][: 4 - len(pvv)] 274 | ) 275 | pvv2 = pvv2.translate({97: 48, 98: 49, 99: 50, 100: 51, 101: 52, 102: 53}) 276 | pvv = pvv + pvv2 277 | 278 | return pvv 279 | -------------------------------------------------------------------------------- /psec/mac.py: -------------------------------------------------------------------------------- 1 | r"""This module implements ISO/IEC 9797-1 MAC algorithms and 2 | padding methods used in retail payments. 3 | 4 | See https://en.wikipedia.org/wiki/ISO/IEC_9797-1 for more information. 5 | """ 6 | 7 | import enum as _enum 8 | import typing as _typing 9 | 10 | from cryptography.hazmat.backends import default_backend as _default_backend 11 | from cryptography.hazmat.primitives.ciphers import Cipher as _Cipher 12 | from cryptography.hazmat.primitives.ciphers import algorithms as _algorithms 13 | from cryptography.hazmat.primitives.ciphers import modes as _modes 14 | 15 | from psec import aes as _aes 16 | from psec import des as _des 17 | 18 | __all__ = [ 19 | "generate_cbc_mac", 20 | "generate_retail_mac", 21 | "pad_iso_1", 22 | "pad_iso_2", 23 | "pad_iso_3", 24 | ] 25 | 26 | _pad_dispatch: _typing.Dict[ 27 | int, _typing.Callable[[bytes, _typing.Optional[int]], bytes] 28 | ] = {} 29 | 30 | 31 | class Algorithm(_enum.Enum): 32 | DES = _enum.auto() 33 | AES = _enum.auto() 34 | 35 | 36 | def generate_cbc_mac( 37 | key: bytes, 38 | data: bytes, 39 | padding: int, 40 | length: _typing.Optional[int] = None, 41 | algorithm: _typing.Optional[Algorithm] = None, 42 | ) -> bytes: 43 | r"""ISO/IEC 9797-1 MAC algorithm 1 aka CBC MAC. 44 | All data blocks are processed using TDES or AES CBC. 45 | The last block is the MAC. 46 | 47 | Parameters 48 | ---------- 49 | key : bytes 50 | Binary MAC key. Has to be a valid DES or AES key. 51 | data : bytes 52 | Data to be MAC'd. 53 | padding : int 54 | Padding method of `data`. 55 | 56 | - 1 = ISO/IEC 9797-1 method 1. 57 | - 2 = ISO/IEC 9797-1 method 2. 58 | - 3 = ISO/IEC 9797-1 method 3. 59 | 60 | length : int, optional 61 | For TDES desired MAC length [4 <= N <= 8] (defaults to 8). 62 | For AES desired MAC length [4 <= N <= 16] (defaults to 16). 63 | algorithm : Algorithm, optional 64 | CBC MAC algorithm. Defaults to DES. 65 | 66 | Returns 67 | ------- 68 | mac : bytes 69 | Returns a binary MAC of requested length 70 | 71 | Raises 72 | ------ 73 | ValueError 74 | Invalid padding method specified 75 | 76 | Notes 77 | ----- 78 | See https://en.wikipedia.org/wiki/ISO/IEC_9797-1 for the 79 | algorithm reference. 80 | 81 | See Also 82 | -------- 83 | psec.mac.pad_iso_1 : ISO/IEC 9791-1 padding method 1 84 | psec.mac.pad_iso_2 : ISO/IEC 9791-1 padding method 2 85 | psec.mac.pad_iso_3 : ISO/IEC 9791-1 padding method 3 86 | 87 | Examples 88 | -------- 89 | >>> import psec 90 | >>> key = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 91 | >>> data = bytes.fromhex("1234567890ABCDEF") 92 | >>> psec.mac.generate_cbc_mac(key, data, padding=2).hex().upper() 93 | '925B1737EF681AD3' 94 | """ 95 | if algorithm is None: 96 | algorithm = Algorithm.DES 97 | 98 | if algorithm == Algorithm.AES: 99 | block_size = 16 100 | implementation = _aes.encrypt_aes_cbc 101 | else: 102 | block_size = 8 103 | implementation = _des.encrypt_tdes_cbc 104 | 105 | if length is None: 106 | length = block_size 107 | 108 | try: 109 | data = _pad_dispatch[padding](data, block_size) 110 | except KeyError: 111 | raise ValueError("Specify valid padding method: 1, 2 or 3.") 112 | 113 | mac = implementation(key, b"\x00" * block_size, data)[-block_size:] 114 | return mac[:length] 115 | 116 | 117 | def generate_retail_mac( 118 | key1: bytes, 119 | key2: bytes, 120 | data: bytes, 121 | padding: int, 122 | length: _typing.Optional[int] = None, 123 | ) -> bytes: 124 | r"""ISO/IEC 9797-1 MAC algorithm 3 aka retail MAC. 125 | Requires two independent keys. 126 | All blocks until the last are processed using single DES using key1. 127 | The last data block is processed using TDES using key2 and key1. 128 | The resulting block is the MAC. 129 | 130 | Parameters 131 | ---------- 132 | key1 : bytes 133 | Binary MAC key used in initial transformation. 134 | Has to be a valid DES key. 135 | key2 : bytes 136 | Binary MAC key used in output transformation. 137 | Has to be a valid DES key. 138 | data : bytes 139 | Data to be MAC'd. 140 | padding : int 141 | Padding method of `data`. 142 | 143 | - 1 = ISO/IEC 9797-1 method 1. 144 | - 2 = ISO/IEC 9797-1 method 2. 145 | - 3 = ISO/IEC 9797-1 method 3. 146 | 147 | length : int, optional 148 | Desired MAC length [4 <= N <= 8] (default 8 bytes). 149 | 150 | Returns 151 | ------- 152 | mac : bytes 153 | Returns a binary MAC of requested length 154 | 155 | Raises 156 | ------ 157 | ValueError 158 | Invalid padding method specified 159 | 160 | Notes 161 | ----- 162 | See https://en.wikipedia.org/wiki/ISO/IEC_9797-1 for the 163 | algorithm reference. 164 | 165 | See Also 166 | -------- 167 | psec.mac.pad_iso_1 : ISO/IEC 9791-1 padding method 1 168 | psec.mac.pad_iso_2 : ISO/IEC 9791-1 padding method 2 169 | psec.mac.pad_iso_3 : ISO/IEC 9791-1 padding method 3 170 | 171 | Examples 172 | -------- 173 | >>> import psec 174 | >>> key1 = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 175 | >>> key2 = bytes.fromhex("FEDCBA98765432100123456789ABCDEF") 176 | >>> data = bytes.fromhex("1234567890ABCDEF") 177 | >>> psec.mac.generate_retail_mac(key1, key2, data, padding=2).hex().upper() 178 | '644AA5C915DBDAF8' 179 | """ 180 | if length is None: 181 | length = 8 182 | 183 | try: 184 | data = _pad_dispatch[padding](data, 8) 185 | except KeyError: 186 | raise ValueError("Specify valid padding method: 1, 2 or 3.") 187 | 188 | # Encrypt first block with key1 then 189 | # encrypt the rest of the data in CBC mode 190 | cipher1 = _Cipher( 191 | _algorithms.TripleDES(key1), 192 | _modes.CBC(b"\x00\x00\x00\x00\x00\x00\x00\x00"), 193 | backend=_default_backend(), 194 | ) 195 | encryptor1 = cipher1.encryptor() 196 | data = encryptor1.update(data)[-8:] 197 | 198 | # Decrypt the last block with key2 and then encrypt it with key1 199 | cipher2 = _Cipher( 200 | _algorithms.TripleDES(key2), _modes.CBC(data), backend=_default_backend() 201 | ) 202 | decryptor2 = cipher2.decryptor() 203 | return encryptor1.update(decryptor2.update(data))[:length] 204 | 205 | 206 | def pad_iso_1(data: bytes, block_size: _typing.Optional[int] = None) -> bytes: 207 | r"""ISO/IEC 9797-1 padding method 1. 208 | Add the smallest number of "0x00" bytes to the right 209 | such that the length of resulting message is a multiple of 210 | `block_size` bytes. If the data is already multiple of 211 | `block_size` bytes then no bytes added 212 | 213 | Parameters 214 | ---------- 215 | data : bytes 216 | Data to be padded 217 | block_size : int, optional 218 | Padded data will be multiple of specified block size (default 8). 219 | 220 | Returns 221 | ------- 222 | bytes 223 | Padded data 224 | 225 | Notes 226 | ----- 227 | See https://en.wikipedia.org/wiki/ISO/IEC_9797-1 for the 228 | algorithm reference. 229 | 230 | Examples 231 | -------- 232 | >>> import psec 233 | >>> psec.mac.pad_iso_1(bytes.fromhex("1234")).hex().upper() 234 | '1234000000000000' 235 | """ 236 | if block_size is None: 237 | block_size = 8 238 | 239 | remainder = len(data) % block_size 240 | if remainder > 0: 241 | return data + (b"\x00" * (block_size - remainder)) 242 | 243 | if len(data) == 0: 244 | return b"\x00" * block_size 245 | 246 | return data 247 | 248 | 249 | _pad_dispatch[1] = pad_iso_1 250 | 251 | 252 | def pad_iso_2(data: bytes, block_size: _typing.Optional[int] = None) -> bytes: 253 | r"""ISO/IEC 9797-1 padding method 2 (equivalent to ISO/IEC 7816-4). 254 | Add a mandatory "0x80" byte to the right of data, 255 | and then add the smallest number of "0x00" bytes to the right 256 | such that the length of resulting message is a multiple of 257 | `block_size` bytes. 258 | 259 | Parameters 260 | ---------- 261 | data : bytes 262 | Data to be padded 263 | block_size : int, optional 264 | Padded data will be multiple of specified block size (default 8). 265 | 266 | Returns 267 | ------- 268 | bytes 269 | Padded data 270 | 271 | Notes 272 | ----- 273 | See https://en.wikipedia.org/wiki/ISO/IEC_9797-1 for the 274 | algorithm reference. 275 | 276 | Examples 277 | -------- 278 | >>> import psec 279 | >>> psec.mac.pad_iso_2(bytes.fromhex("1234")).hex().upper() 280 | '1234800000000000' 281 | """ 282 | if block_size is None: 283 | block_size = 8 284 | 285 | return pad_iso_1(data + b"\x80", block_size) 286 | 287 | 288 | _pad_dispatch[2] = pad_iso_2 289 | 290 | 291 | def pad_iso_3(data: bytes, block_size: _typing.Optional[int] = None) -> bytes: 292 | r"""ISO/IEC 9797-1 padding method 3. 293 | The padded data comprises (in this order): 294 | 295 | - The length of the unpadded data (in bits) expressed 296 | in big-endian binary in `block_size` bits (i.e. one `block_size`) 297 | - The unpadded data 298 | - As many (possibly none) bits with value 0 as are required to bring 299 | the total length to a multiple of `block_size` bits 300 | 301 | Parameters 302 | ---------- 303 | data : bytes 304 | Data to be padded 305 | block_size : int, optional 306 | Padded data will be multiple of specified block size (default 8). 307 | 308 | Returns 309 | ------- 310 | bytes 311 | Padded data 312 | 313 | Notes 314 | ----- 315 | See https://en.wikipedia.org/wiki/ISO/IEC_9797-1 for the 316 | algorithm reference. 317 | 318 | Examples 319 | -------- 320 | >>> import psec 321 | >>> psec.mac.pad_iso_3(bytes.fromhex("1234")).hex().upper() 322 | '00000000000000101234000000000000' 323 | """ 324 | if block_size is None: 325 | block_size = 8 326 | 327 | return (len(data) * 8).to_bytes(block_size, "big") + pad_iso_1(data, block_size) 328 | 329 | 330 | _pad_dispatch[3] = pad_iso_3 331 | -------------------------------------------------------------------------------- /tests/test_pin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import psec 3 | 4 | 5 | # fmt: off 6 | @pytest.mark.parametrize( 7 | ["pvk", "conversion_table", "offset", "pan", "pan_verify_offset", "pan_verify_length", "pan_pad", "error"], 8 | [ 9 | (b"1234", "1234567890123456", "0000", "1122334455667788", 0, 16, "F", "PVK must be a DES key"), 10 | (b"12345678", "123456789012345", "0000", "1122334455667788", 0, 16, "F", "Conversion table must 16 digits"), 11 | (b"12345678", "12345678901234567", "0000", "1122334455667788", 0, 16, "F", "Conversion table must 16 digits"), 12 | (b"12345678", "123456789012345A", "0000", "1122334455667788", 0, 16, "F", "Conversion table must 16 digits"), 13 | (b"12345678", "1234567890123456", "000", "1122334455667788", 0, 16, "F", "Offset must be from 4 to 16 digits"), 14 | (b"12345678", "1234567890123456", "00000000000000000", "1122334455667788", 0, 16, "F", "Offset must be from 4 to 16 digits"), 15 | (b"12345678", "1234567890123456", "000A", "1122334455667788", 0, 16, "F", "Offset must be from 4 to 16 digits"), 16 | (b"12345678", "1234567890123456", "0000", "11223344556677889900", 0, 16, "F", "PAN must be less than 19 digits"), 17 | (b"12345678", "1234567890123456", "0000", "112233445566778A", 0, 16, "F", "PAN must be less than 19 digits"), 18 | (b"12345678", "1234567890123456", "0000", "1122334455667788", 0, 16, "", "PAN pad character must be valid hex digit"), 19 | (b"12345678", "1234567890123456", "0000", "1122334455667788", 0, 16, "FF", "PAN pad character must be valid hex digit"), 20 | (b"12345678", "1234567890123456", "0000", "1122334455667788", 0, 16, "X", "PAN pad character must be valid hex digit"), 21 | (b"12345678", "1234567890123456", "0000", "1122334455667788", 1, 16, "F", "PAN verify offset and length must be within provided PAN"), 22 | (b"12345678", "1234567890123456", "0000", "1122334455667788", 0, 17, "F", "PAN verify offset and length must be within provided PAN"), 23 | ], 24 | ) 25 | # fmt: on 26 | def test_generate_ibm3624_pin_exceptions( 27 | pvk: bytes, 28 | conversion_table: str, 29 | offset: str, 30 | pan: str, 31 | pan_verify_offset: int, 32 | pan_verify_length: int, 33 | pan_pad: str, 34 | error: str, 35 | ) -> None: 36 | with pytest.raises(ValueError, match=error): 37 | psec.pin.generate_ibm3624_pin( 38 | pvk, 39 | conversion_table, 40 | offset, 41 | pan, 42 | pan_verify_offset, 43 | pan_verify_length, 44 | pan_pad, 45 | ) 46 | 47 | 48 | # fmt: off 49 | @pytest.mark.parametrize( 50 | ["conversion_table", "offset", "pan", "pan_verify_offset", "pan_verify_length", "pan_pad", "result_pin"], 51 | [ 52 | ("1234567890123456", "0000", "1122334455667788", 0, 16, "F", "4524"), 53 | ("1234567890123456", "0000", "1122334455667788", 0, 16, "f", "4524"), 54 | ("1234567890123456", "1111", "1122334455667788", 0, 16, "F", "5635"), 55 | ("1234567890123456", "6586", "1122334455667788", 0, 16, "F", "0000"), 56 | ("1234567890123456", "7697", "1122334455667788", 0, 16, "F", "1111"), 57 | ("1234567890123456", "7710", "1122334455667788", 0, 16, "F", "1234"), 58 | ("1234567890123456", "0000", "1122334455667788", 0, 14, "F", "5518"), 59 | ("1234567890123456", "0000", "1122334455667788", 0, 14, "f", "5518"), 60 | ("1234567890123456", "0000", "1122334455667788", 1, 14, "1", "3675"), 61 | ("1234567890123456", "0000", "1122334455667788", 2, 14, "1", "5550"), 62 | ], 63 | ) 64 | # fmt: on 65 | def test_generate_ibm3624_pin( 66 | conversion_table: str, 67 | offset: str, 68 | pan: str, 69 | pan_verify_offset: int, 70 | pan_verify_length: int, 71 | pan_pad: str, 72 | result_pin: str, 73 | ) -> None: 74 | pvk = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 75 | pin = psec.pin.generate_ibm3624_pin( 76 | pvk, 77 | conversion_table, 78 | offset, 79 | pan, 80 | pan_verify_offset, 81 | pan_verify_length, 82 | pan_pad, 83 | ) 84 | assert result_pin == pin 85 | 86 | 87 | # fmt: off 88 | @pytest.mark.parametrize( 89 | ["pvk", "conversion_table", "pin", "pan", "pan_verify_offset", "pan_verify_length", "pan_pad", "error"], 90 | [ 91 | (b"1234", "1234567890123456", "0000", "1122334455667788", 0, 16, "F", "PVK must be a DES key"), 92 | (b"12345678", "123456789012345", "0000", "1122334455667788", 0, 16, "F", "Conversion table must 16 digits"), 93 | (b"12345678", "12345678901234567", "0000", "1122334455667788", 0, 16, "F", "Conversion table must 16 digits"), 94 | (b"12345678", "123456789012345A", "0000", "1122334455667788", 0, 16, "F", "Conversion table must 16 digits"), 95 | (b"12345678", "1234567890123456", "000", "1122334455667788", 0, 16, "F", "PIN must be from 4 to 16 digits"), 96 | (b"12345678", "1234567890123456", "00000000000000000", "1122334455667788", 0, 16, "F", "PIN must be from 4 to 16 digits"), 97 | (b"12345678", "1234567890123456", "000A", "1122334455667788", 0, 16, "F", "PIN must be from 4 to 16 digits"), 98 | (b"12345678", "1234567890123456", "0000", "11223344556677889900", 0, 16, "F", "PAN must be less than 19 digits"), 99 | (b"12345678", "1234567890123456", "0000", "112233445566778A", 0, 16, "F", "PAN must be less than 19 digits"), 100 | (b"12345678", "1234567890123456", "0000", "1122334455667788", 0, 16, "", "PAN pad character must be valid hex digit"), 101 | (b"12345678", "1234567890123456", "0000", "1122334455667788", 0, 16, "FF", "PAN pad character must be valid hex digit"), 102 | (b"12345678", "1234567890123456", "0000", "1122334455667788", 0, 16, "X", "PAN pad character must be valid hex digit"), 103 | (b"12345678", "1234567890123456", "0000", "1122334455667788", 1, 16, "F", "PAN verify offset and length must be within provided PAN"), 104 | (b"12345678", "1234567890123456", "0000", "1122334455667788", 0, 17, "F", "PAN verify offset and length must be within provided PAN"), 105 | ], 106 | ) 107 | # fmt: on 108 | def test_generate_ibm3624_offset_exceptions( 109 | pvk: bytes, 110 | conversion_table: str, 111 | pin: str, 112 | pan: str, 113 | pan_verify_offset: int, 114 | pan_verify_length: int, 115 | pan_pad: str, 116 | error: str, 117 | ) -> None: 118 | with pytest.raises(ValueError, match=error): 119 | psec.pin.generate_ibm3624_offset( 120 | pvk, 121 | conversion_table, 122 | pin, 123 | pan, 124 | pan_verify_offset, 125 | pan_verify_length, 126 | pan_pad, 127 | ) 128 | 129 | 130 | # fmt: off 131 | @pytest.mark.parametrize( 132 | ["conversion_table", "pin", "pan", "pan_verify_offset", "pan_verify_length", "pan_pad", "result_offset"], 133 | [ 134 | ("1234567890123456", "4524", "1122334455667788", 0, 16, "F", "0000"), 135 | ("1234567890123456", "4524", "1122334455667788", 0, 16, "f", "0000"), 136 | ("1234567890123456", "5635", "1122334455667788", 0, 16, "F", "1111"), 137 | ("1234567890123456", "0000", "1122334455667788", 0, 16, "F", "6586"), 138 | ("1234567890123456", "1111", "1122334455667788", 0, 16, "F", "7697"), 139 | ("1234567890123456", "1234", "1122334455667788", 0, 16, "F", "7710"), 140 | ("1234567890123456", "5518", "1122334455667788", 0, 14, "F", "0000"), 141 | ("1234567890123456", "5518", "1122334455667788", 0, 14, "f", "0000"), 142 | ("1234567890123456", "3675", "1122334455667788", 1, 14, "1", "0000"), 143 | ("1234567890123456", "5550", "1122334455667788", 2, 14, "1", "0000"), 144 | ], 145 | ) 146 | # fmt: on 147 | def test_generate_ibm3624_offset( 148 | conversion_table: str, 149 | pin: str, 150 | pan: str, 151 | pan_verify_offset: int, 152 | pan_verify_length: int, 153 | pan_pad: str, 154 | result_offset: str, 155 | ) -> None: 156 | pvk = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 157 | offset = psec.pin.generate_ibm3624_offset( 158 | pvk, 159 | conversion_table, 160 | pin, 161 | pan, 162 | pan_verify_offset, 163 | pan_verify_length, 164 | pan_pad, 165 | ) 166 | assert result_offset == offset 167 | 168 | 169 | # fmt: off 170 | @pytest.mark.parametrize( 171 | ["pvk", "pvki", "pin", "pan", "error"], 172 | [ 173 | (b"1234", "1", "0000", "1122334455667788", "PVK must be a DES key"), 174 | (b"", "1", "0000", "1122334455667788", "PVK must be a DES key"), 175 | (b"12345678", "A", "0000", "1122334455667788", 'PVKI must be 1 digit from "0" to "9"'), 176 | (b"12345678", "11", "0000", "1122334455667788", 'PVKI must be 1 digit from "0" to "9"'), 177 | (b"12345678", "", "0000", "1122334455667788", 'PVKI must be 1 digit from "0" to "9"'), 178 | (b"12345678", "1", "000", "1122334455667788", "PIN must be 4 digits"), 179 | (b"12345678", "1", "00000", "1122334455667788", "PIN must be 4 digits"), 180 | (b"12345678", "1", "A000", "1122334455667788", "PIN must be 4 digits"), 181 | (b"12345678", "1", "000D", "1122334455667788", "PIN must be 4 digits"), 182 | (b"12345678", "1", "", "1122334455667788", "PIN must be 4 digits"), 183 | (b"12345678", "1", "0000", "11223344556", "PAN must be more than 12 digits"), 184 | (b"12345678", "1", "0000", "1122334455A", "PAN must be more than 12 digits"), 185 | (b"12345678", "1", "0000", "", "PAN must be more than 12 digits"), 186 | ], 187 | ) 188 | # fmt: on 189 | def test_generate_visa_pvv_exceptions( 190 | pvk: bytes, 191 | pvki: str, 192 | pin: str, 193 | pan: str, 194 | error: str, 195 | ) -> None: 196 | with pytest.raises(ValueError, match=error): 197 | psec.pin.generate_visa_pvv(pvk, pvki, pin, pan) 198 | 199 | 200 | # fmt: off 201 | @pytest.mark.parametrize( 202 | ["pvki", "pin", "pan", "result_pvv"], 203 | [ 204 | ("1", "4524", "1122334455667788", "8523"), 205 | ("2", "1912", "1122334455667788", "3244"), 206 | ("1", "0570", "1122334455667718", "3144"), 207 | ("1", "8299", "1122334455667708", "4422"), 208 | ], 209 | ) 210 | # fmt: on 211 | def test_generate_visa_pvv( 212 | pvki: str, 213 | pin: str, 214 | pan: str, 215 | result_pvv: str, 216 | ) -> None: 217 | pvk = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210") 218 | assert result_pvv == psec.pin.generate_visa_pvv( 219 | pvk, 220 | pvki, 221 | pin, 222 | pan, 223 | ) 224 | -------------------------------------------------------------------------------- /tests/test_pinblock.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from psec import pinblock 3 | 4 | import secrets as _secrets 5 | from os import urandom as _urandom 6 | 7 | 8 | # fmt: off 9 | @pytest.mark.parametrize( 10 | ["pin", "pan", "error"], 11 | [ 12 | ("123", "55555555555555", "PIN must be between 4 and 12 digits long"), 13 | ("1234567890123", "55555555555555", "PIN must be between 4 and 12 digits long"), 14 | ("123A", "55555555555555", "PIN must be between 4 and 12 digits long"), 15 | ("1234", "555555555555", "PAN must be at least 13 digits long"), 16 | ("1234", "5555555555555A", "PAN must be at least 13 digits long"), 17 | ], 18 | ) 19 | # fmt: on 20 | def test_encode_pinblock_iso_0_exception(pin: str, pan: str, error: str) -> None: 21 | with pytest.raises( 22 | ValueError, 23 | match=error, 24 | ): 25 | pinblock.encode_pinblock_iso_0(pin, pan) 26 | 27 | 28 | @pytest.mark.parametrize( 29 | ["pin", "pan", "pin_block"], 30 | [ 31 | ("1234", "5555555551234567", "041261AAAAEDCBA9"), 32 | ("123456789012", "5555555551234567", "0C1261032D8226A9"), 33 | ], 34 | ) 35 | def test_encode_pinblock_iso_0(pin: str, pan: str, pin_block: str) -> None: 36 | assert pin_block == pinblock.encode_pinblock_iso_0(pin, pan).hex().upper() 37 | 38 | 39 | # fmt: off 40 | @pytest.mark.parametrize( 41 | ["pin", "error"], 42 | [ 43 | ("123", "PIN must be between 4 and 12 digits long"), 44 | ("1234567890123", "PIN must be between 4 and 12 digits long"), 45 | ("123A", "PIN must be between 4 and 12 digits long"), 46 | ], 47 | ) 48 | # fmt: on 49 | def test_encode_pinblock_iso_2_exception(pin: str, error: str) -> None: 50 | with pytest.raises( 51 | ValueError, 52 | match=error, 53 | ): 54 | pinblock.encode_pinblock_iso_2(pin) 55 | 56 | 57 | @pytest.mark.parametrize( 58 | ["pin", "pin_block"], 59 | [ 60 | ("1234", "241234FFFFFFFFFF"), 61 | ("123456789", "29123456789FFFFF"), 62 | ("1234567890", "2A1234567890FFFF"), 63 | ("123456789012", "2C123456789012FF"), 64 | ], 65 | ) 66 | def test_encode_pinblock_iso_2(pin: str, pin_block: str) -> None: 67 | assert pin_block == pinblock.encode_pinblock_iso_2(pin).hex().upper() 68 | 69 | 70 | # fmt: off 71 | @pytest.mark.parametrize( 72 | ["pin", "pan", "error"], 73 | [ 74 | ("123", "55555555555555", "PIN must be between 4 and 12 digits long"), 75 | ("1234567890123", "55555555555555", "PIN must be between 4 and 12 digits long"), 76 | ("123A", "55555555555555", "PIN must be between 4 and 12 digits long"), 77 | ("1234", "555555555555", "PAN must be at least 13 digits long"), 78 | ("1234", "5555555555555A", "PAN must be at least 13 digits long"), 79 | ], 80 | ) 81 | # fmt: on 82 | def test_encode_pinblock_iso_3_exception(pin: str, pan: str, error: str) -> None: 83 | with pytest.raises( 84 | ValueError, 85 | match=error, 86 | ): 87 | pinblock.encode_pinblock_iso_3(pin, pan) 88 | 89 | 90 | @pytest.mark.parametrize( 91 | ["pin", "pan", "pin_block"], 92 | [ 93 | ("1234", "5555555551234567", "341261"), 94 | ("123456789012", "5555555551234567", "3C1261032D8226"), 95 | ], 96 | ) 97 | def test_encode_pinblock_iso_3(pin: str, pan: str, pin_block: str) -> None: 98 | assert ( 99 | pin_block 100 | == pinblock.encode_pinblock_iso_3(pin, pan).hex().upper()[: 2 + len(pin)] 101 | ) 102 | 103 | 104 | @pytest.mark.parametrize( 105 | ["pin", "error"], 106 | [ 107 | ("123", "PIN must be between 4 and 12 digits long"), 108 | ("1234567890123", "PIN must be between 4 and 12 digits long"), 109 | ], 110 | ) 111 | def test_encode_pin_field_iso_4_exception(pin: str, error: str) -> None: 112 | with pytest.raises( 113 | ValueError, 114 | match=error, 115 | ): 116 | pinblock.encode_pin_field_iso_4(pin) 117 | 118 | 119 | @pytest.mark.parametrize( 120 | ["pin", "pin_field"], 121 | [ 122 | ("1234", "441234AAAAAAAAAA"), 123 | ], 124 | ) 125 | def test_encode_pin_field_iso_4(pin: str, pin_field: str) -> None: 126 | assert pin_field == pinblock.encode_pin_field_iso_4(pin).hex().upper()[:16] 127 | 128 | 129 | @pytest.mark.parametrize( 130 | ["pan", "error"], 131 | [ 132 | ("", "PAN must be between 1 and 19 digits long."), 133 | ("12345678901234567890", "PAN must be between 1 and 19 digits long."), 134 | ], 135 | ) 136 | # fmt: on 137 | def test_encode_pan_field_iso_4_exception(pan: str, error: str) -> None: 138 | with pytest.raises( 139 | ValueError, 140 | match=error, 141 | ): 142 | pinblock.encode_pan_field_iso_4(pan) 143 | 144 | 145 | @pytest.mark.parametrize( 146 | ["pan", "pan_field"], 147 | [ 148 | ("112233445566778899", "61122334455667788990000000000000"), 149 | ], 150 | ) 151 | # fmt: on 152 | def test_encode_pan_field_iso_4(pan: str, pan_field: str) -> None: 153 | assert pan_field == pinblock.encode_pan_field_iso_4(pan).hex().upper() 154 | 155 | 156 | # fmt: off 157 | @pytest.mark.parametrize( 158 | ["key", "pin", "pan", "error"], 159 | [ 160 | (b"1" * 16, "123", "55555555555555", "PIN must be between 4 and 12 digits long"), 161 | (b"1" * 16, "1234567890123", "55555555555555", "PIN must be between 4 and 12 digits long"), 162 | (b"1" * 16, "123A", "55555555555555", "PIN must be between 4 and 12 digits long"), 163 | (b"1" * 16, "1234", "", "PAN must be between 1 and 19 digits long."), 164 | (b"1" * 16, "1234", "555555555555555555555", "PAN must be between 1 and 19 digits long."), 165 | (b"1" * 16, "1234", "5555555555555A", "PAN must be between 1 and 19 digits long."), 166 | ], 167 | ) 168 | # fmt: on 169 | def test_encipher_pinblock_iso_4_exception( 170 | key: bytes, pin: str, pan: str, error: str 171 | ) -> None: 172 | with pytest.raises( 173 | ValueError, 174 | match=error, 175 | ): 176 | pinblock.encipher_pinblock_iso_4(key, pin, pan) 177 | 178 | 179 | # deterministic 180 | @pytest.mark.parametrize( 181 | ["key", "pin", "pan", "pin_block"], 182 | [ 183 | ( 184 | bytes.fromhex("00112233445566778899AABBCCDDEEFF"), 185 | "1234", 186 | "1234567890123456789", 187 | "28B41FDDD29B743E93124BD8E32D921E", 188 | ), 189 | ], 190 | ) 191 | def test_encipher_pinblock_iso_4_det( 192 | key: bytes, pin: str, pan: str, pin_block: str 193 | ) -> None: 194 | pinblock._urandom = lambda n: bytes.fromhex("FF") * n 195 | assert pin_block == pinblock.encipher_pinblock_iso_4(key, pin, pan).hex().upper() 196 | 197 | 198 | # non deterministic 199 | def test_encipher_decipher_pinblock_iso_4_non_det() -> None: 200 | pin_len = _secrets.choice(range(4, 12)) 201 | pin_range_start = 10 ** (pin_len - 1) 202 | pin_range_end = (10**pin_len) - 1 203 | pin = str(_secrets.choice(range(pin_range_start, pin_range_end))) 204 | pan = "12334567890123456" 205 | key_lens = [16, 24, 32] 206 | key_len = _secrets.choice(key_lens) 207 | key = _urandom(key_len) 208 | pin_block = pinblock.encipher_pinblock_iso_4(key, pin, pan) 209 | pin_calc = pinblock.decipher_pinblock_iso_4(key, pin_block, pan) 210 | 211 | assert pin == pin_calc 212 | 213 | 214 | # fmt: off 215 | @pytest.mark.parametrize( 216 | ["pin_block", "pan", "error"], 217 | [ 218 | (bytes.fromhex("241261AAAAEDCBA9"), "5555555551234567", "PIN block is not ISO format 0: control field `2`"), 219 | (bytes.fromhex("041261AAAAEDCB"), "5555555551234567", "PIN block must be 8 bytes long"), 220 | (bytes.fromhex("0F1261AAAAEDCBA9"), "5555555551234567", "PIN length must be between 4 and 12: `15`"), 221 | (bytes.fromhex("021261AAAAEDCBA9"), "5555555551234567", "PIN length must be between 4 and 12: `2`"), 222 | (bytes.fromhex("041261AAAAEDCBAA"), "5555555551234567", "PIN block filler is incorrect: `FFFFFFFFFC`"), 223 | (bytes.fromhex("051261AAAAEDCBA9"), "5555555551234567", "PIN is not numeric: `1234F`"), 224 | (bytes.fromhex("051261AAAAEDCBA9"), "555555555123", "PAN must be at least 13 digits long"), 225 | (bytes.fromhex("051261AAAAEDCBA9"), "5555555551234A", "PAN must be at least 13 digits long"), 226 | ], 227 | ) 228 | # fmt: on 229 | def test_decode_pinblock_iso_0_exception( 230 | pin_block: bytes, pan: str, error: str 231 | ) -> None: 232 | with pytest.raises( 233 | ValueError, 234 | match=error, 235 | ): 236 | pinblock.decode_pinblock_iso_0(pin_block, pan) 237 | 238 | 239 | @pytest.mark.parametrize( 240 | ["pin", "pin_block", "pan"], 241 | [ 242 | ("1234", bytes.fromhex("041261AAAAEDCBA9"), "5555555551234567"), 243 | ("123456789", bytes.fromhex("091261032D8DCBA9"), "5555555551234567"), 244 | ("1234567890", bytes.fromhex("0A1261032D82CBA9"), "5555555551234567"), 245 | ("123456789012", bytes.fromhex("0C1261032D8226A9"), "5555555551234567"), 246 | ], 247 | ) 248 | def test_decode_pinblock_iso_0(pin: str, pin_block: bytes, pan: str) -> None: 249 | assert pin == pinblock.decode_pinblock_iso_0(pin_block, pan) 250 | 251 | 252 | # fmt: off 253 | @pytest.mark.parametrize( 254 | ["pin_block", "error"], 255 | [ 256 | (bytes.fromhex("041234FFFFFFFFFF"), "PIN block is not ISO format 2: control field `0`"), 257 | (bytes.fromhex("29123456789FFF"), "PIN block must be 8 bytes long"), 258 | (bytes.fromhex("2F123456789012FF"), "PIN length must be between 4 and 12: `15`"), 259 | (bytes.fromhex("22123456789012FF"), "PIN length must be between 4 and 12: `2`"), 260 | (bytes.fromhex("2C123456789012CF"), "PIN block filler is incorrect: `CF`"), 261 | (bytes.fromhex("2C12345678901FFF"), "PIN is not numeric: `12345678901F`"), 262 | ], 263 | ) 264 | # fmt: on 265 | def test_decode_pinblock_iso_2_exception(pin_block: bytes, error: str) -> None: 266 | with pytest.raises( 267 | ValueError, 268 | match=error, 269 | ): 270 | pinblock.decode_pinblock_iso_2(pin_block) 271 | 272 | 273 | @pytest.mark.parametrize( 274 | ["pin", "pin_block"], 275 | [ 276 | ("1234", bytes.fromhex("241234FFFFFFFFFF")), 277 | ("123456789", bytes.fromhex("29123456789FFFFF")), 278 | ("1234567890", bytes.fromhex("2A1234567890FFFF")), 279 | ("123456789012", bytes.fromhex("2C123456789012FF")), 280 | ], 281 | ) 282 | def test_decode_pinblock_iso_2(pin: str, pin_block: bytes) -> None: 283 | assert pin == pinblock.decode_pinblock_iso_2(pin_block) 284 | 285 | 286 | # fmt: off 287 | @pytest.mark.parametrize( 288 | ["pin_block", "pan", "error"], 289 | [ 290 | (bytes.fromhex("241261AAAAEDCBA9"), "5555555551234567", "PIN block is not ISO format 3: control field `2`"), 291 | (bytes.fromhex("341261AAAAEDCB"), "5555555551234567", "PIN block must be 8 bytes long"), 292 | (bytes.fromhex("3F1261AAAAEDCBA9"), "5555555551234567", "PIN length must be between 4 and 12: `15`"), 293 | (bytes.fromhex("321261AAAAEDCBA9"), "5555555551234567", "PIN length must be between 4 and 12: `2`"), 294 | (bytes.fromhex("341261AAAAEDCBA2"), "5555555551234567", "PIN block filler is incorrect: `FFFFFFFFF4`"), 295 | (bytes.fromhex("351261AAAAEDCBA9"), "5555555551234567", "PIN is not numeric: `1234F`"), 296 | (bytes.fromhex("351261AAAAEDCBA9"), "555555555123", "PAN must be at least 13 digits long"), 297 | (bytes.fromhex("351261AAAAEDCBA9"), "5555555551234A", "PAN must be at least 13 digits long"), 298 | ], 299 | ) 300 | # fmt: on 301 | def test_decode_pinblock_iso_3_exception( 302 | pin_block: bytes, pan: str, error: str 303 | ) -> None: 304 | with pytest.raises( 305 | ValueError, 306 | match=error, 307 | ): 308 | pinblock.decode_pinblock_iso_3(pin_block, pan) 309 | 310 | 311 | @pytest.mark.parametrize( 312 | ["pin", "pin_block", "pan"], 313 | [ 314 | ("1234", bytes.fromhex("341261AAAAEDCBA9"), "5555555551234567"), 315 | ("123456789", bytes.fromhex("391261032D8DCBA9"), "5555555551234567"), 316 | ("1234567890", bytes.fromhex("3A1261032D82CBA9"), "5555555551234567"), 317 | ("123456789012", bytes.fromhex("3C1261032D8226A9"), "5555555551234567"), 318 | ], 319 | ) 320 | def test_decode_pinblock_iso_3(pin: str, pin_block: bytes, pan: str) -> None: 321 | assert pin == pinblock.decode_pinblock_iso_3(pin_block, pan) 322 | 323 | 324 | # fmt: off 325 | @pytest.mark.parametrize( 326 | ["pin_block", "error"], 327 | [ 328 | (bytes.fromhex("441234AAAAAAAAAA548ED7FD6549595000"), "PIN field must be 16 bytes long"), 329 | (bytes.fromhex("341261AAAAEDCB"), "PIN field must be 16 bytes long"), 330 | (bytes.fromhex("541234AAAAAAAAAA548ED7FD65495950"), "PIN block is not ISO format 4: control field `5`"), 331 | (bytes.fromhex("43123AAAAAAAAAAA548ED7FD65495950"), "PIN length must be between 4 and 12: `3`"), 332 | (bytes.fromhex("441234ABCDEFABCD548ED7FD65495950"), "PIN block filler is incorrect: `ABCDEFABCD`"), 333 | (bytes.fromhex("4412D4AAAAAAAAAA548ED7FD65495950"), "PIN is not numeric: `12D4`"), 334 | ], 335 | ) 336 | # fmt: on 337 | def test_decode_pinblock_iso_4_exception(pin_block: bytes, error: str) -> None: 338 | with pytest.raises( 339 | ValueError, 340 | match=error, 341 | ): 342 | pinblock.decode_pin_field_iso_4(pin_block) 343 | 344 | 345 | @pytest.mark.parametrize( 346 | ["pin_field", "pin"], 347 | [ 348 | (bytes.fromhex("441234AAAAAAAAAA548ED7FD65495950"), "1234"), 349 | ], 350 | ) 351 | def test_decode_pin_field_iso_4(pin_field: bytes, pin) -> None: 352 | assert pin == pinblock.decode_pin_field_iso_4(pin_field) 353 | 354 | 355 | @pytest.mark.parametrize( 356 | ["key", "pin_block", "pan", "error"], 357 | [ 358 | ( 359 | bytes.fromhex("C1D0F8FB4958670DBA40AB1F3752EF0D"), 360 | bytes.fromhex("CC17F65586BFD0953010226C4FC5B3CA00"), 361 | "432198765432109870", 362 | "Data length (17) must be multiple of AES block size 16.", 363 | ), 364 | ( 365 | bytes.fromhex("C1D0F8FB4958670DBA40AB1F3752EF0D"), 366 | bytes.fromhex("CC17F65586BFD0953010226C4FC5B3CA"), 367 | "43219876543210987099", 368 | "PAN must be between 1 and 19 digits long.", 369 | ), 370 | ( 371 | bytes.fromhex("E60B15B90ABDF14CEE337C97440F0D6E"), 372 | bytes.fromhex("7D5AF4C33667A2098626027FB7A9A1B7"), 373 | "4266229809609384667", 374 | "PIN block is not ISO format 4: control field `3`", 375 | ), 376 | ( 377 | bytes.fromhex("C7BBEBA16C5AD97D5866450718C03750"), 378 | bytes.fromhex("2F9C910447F17293EC95DEDE1E3CA201"), 379 | "8220499325458689523", 380 | "PIN is not numeric: `1AB2`", 381 | ), 382 | ( 383 | bytes.fromhex("820F6C7C12355BDFF1AB6CE12E8EED89"), 384 | bytes.fromhex("CCF310A8300B46C925A86B1098089301"), 385 | "939589847393485609", 386 | "PIN block filler is incorrect: `BBBBBBBBBB`", 387 | ), 388 | ( 389 | bytes.fromhex("4FE3F0311936FBCE44F17159F1659CF09B2BF8913BB514A1"), 390 | bytes.fromhex("51334B00A6CBA3EC7B1C6F871F060AFC"), 391 | "2129100799029059903", 392 | "PIN length must be between 4 and 12: `3`", 393 | ), 394 | ], 395 | ) 396 | # fmt: on 397 | def test_decipher_pinblock_iso_4_exception( 398 | key: bytes, pin_block: bytes, pan: str, error: str 399 | ) -> None: 400 | with pytest.raises(ValueError) as e: 401 | pinblock.decipher_pinblock_iso_4(key, pin_block, pan) 402 | assert e.value.args[0] == error 403 | 404 | 405 | @pytest.mark.parametrize( 406 | ["key", "pin_block", "pan", "pin"], 407 | [ 408 | # Test vector from ep2 - eft/pos 2000 Security Specification, Version 8.0.0, 8.4: PIN Encryption 409 | ( 410 | bytes.fromhex("C1D0F8FB4958670DBA40AB1F3752EF0D"), 411 | bytes.fromhex("CC17F65586BFD0953010226C4FC5B3CA"), 412 | "432198765432109870", 413 | "1234", 414 | ), 415 | # PAN length < 12 416 | ( 417 | bytes.fromhex("00112233445566778899AABBCCDDEEFF"), 418 | bytes.fromhex("39B69B1B91FE05D48F7EF0D68EB2CBD6"), 419 | "1", 420 | "123456", 421 | ), 422 | ], 423 | ) 424 | def test_decipher_pinblock_iso_4( 425 | key: bytes, pin_block: bytes, pan: str, pin: str 426 | ) -> None: 427 | assert pin == pinblock.decipher_pinblock_iso_4(key, pin_block, pan) 428 | -------------------------------------------------------------------------------- /psec/pinblock.py: -------------------------------------------------------------------------------- 1 | r"""PIN blocks are data blocks that contain PIN, pad characters and sometimes 2 | other additional information, such as the length of the PIN. 3 | """ 4 | 5 | import binascii as _binascii 6 | import secrets as _secrets 7 | from os import urandom as _urandom 8 | 9 | from psec import tools as _tools 10 | from psec import aes as _aes 11 | 12 | __all__ = [ 13 | "encode_pinblock_iso_0", 14 | "encode_pinblock_iso_2", 15 | "encode_pinblock_iso_3", 16 | "encode_pin_field_iso_4", 17 | "encode_pan_field_iso_4", 18 | "encipher_pinblock_iso_4", 19 | "decode_pinblock_iso_0", 20 | "decode_pinblock_iso_2", 21 | "decode_pinblock_iso_3", 22 | "decode_pin_field_iso_4", 23 | "decipher_pinblock_iso_4", 24 | ] 25 | 26 | 27 | def encode_pinblock_iso_0(pin: str, pan: str) -> bytes: 28 | r"""Encode ISO 9564 PIN block format 0 aka ANSI PIN block. 29 | ISO format 0 PIN block is an 8 byte value that consits of 30 | 31 | - Control field. A 4 bit hex value set to 0. 32 | - PIN length. A 4 bit hex value in the range from 4 to C. 33 | - PIN digits. Each digit is a 4 bit hex value in the range from 0 to 9. 34 | - Pad character. A 4 bit hex value set to F. 35 | 36 | The PIN block is then XOR'd by an ANSI PAN block that consists of 37 | 38 | - 4 pad characters. Each is 4 bit hex value set to 0. 39 | - 12 rightmost digits of the PAN excluding the check digit. 40 | 41 | Parameters 42 | ---------- 43 | pin : str 44 | ASCII Personal Identification Number. 45 | pan : str 46 | ASCII Personal Account Number. 47 | 48 | Returns 49 | ------- 50 | pinblock : bytes 51 | Binary 8-byte PIN block. 52 | 53 | Raises 54 | ------ 55 | ValueError 56 | PIN must be between 4 and 12 digits long 57 | PAN must be at least 13 digits long 58 | 59 | Examples 60 | -------- 61 | >>> from psec.pinblock import encode_pinblock_iso_0 62 | >>> encode_pinblock_iso_0("1234", "5544332211009966").hex().upper() 63 | '041277CDDEEFF669' 64 | """ 65 | 66 | if len(pin) < 4 or len(pin) > 12 or not _tools.ascii_numeric(pin): 67 | raise ValueError("PIN must be between 4 and 12 digits long") 68 | 69 | if len(pan) < 13 or not _tools.ascii_numeric(pan): 70 | raise ValueError("PAN must be at least 13 digits long") 71 | 72 | pinblock = len(pin).to_bytes(1, "big") + _binascii.a2b_hex( 73 | pin + "F" * (14 - len(pin)) 74 | ) 75 | pan_block = b"\x00\x00" + _binascii.a2b_hex(pan[-13:-1]) 76 | 77 | return _tools.xor(pinblock, pan_block) 78 | 79 | 80 | def encode_pinblock_iso_2(pin: str) -> bytes: 81 | r"""Encode ISO 9564 PIN block format 2. 82 | ISO format 2 PIN block is an 8 byte value that consits of 83 | 84 | - Control field. A 4 bit hex value set to 2. 85 | - PIN length. A 4 bit hex value in the range from 4 to C. 86 | - PIN digits. Each digit is a 4 bit hex value in the range from 0 to 9. 87 | - Pad character. A 4 bit hex value set to F. 88 | 89 | Parameters 90 | ---------- 91 | pin : str 92 | ASCII Personal Identification Number. 93 | 94 | Returns 95 | ------- 96 | pinblock : bytes 97 | Binary 8-byte PIN block. 98 | 99 | Raises 100 | ------ 101 | ValueError 102 | PIN must be between 4 and 12 digits long 103 | 104 | Examples 105 | -------- 106 | >>> from psec.pinblock import encode_pinblock_iso_2 107 | >>> encode_pinblock_iso_2("1234").hex().upper() 108 | '241234FFFFFFFFFF' 109 | """ 110 | 111 | if len(pin) < 4 or len(pin) > 12 or not _tools.ascii_numeric(pin): 112 | raise ValueError("PIN must be between 4 and 12 digits long") 113 | 114 | return (len(pin) + 32).to_bytes(1, "big") + _binascii.a2b_hex( 115 | pin + "F" * (14 - len(pin)) 116 | ) 117 | 118 | 119 | def encode_pinblock_iso_3(pin: str, pan: str) -> bytes: 120 | r"""Encode ISO 9564 PIN block format 3. 121 | ISO format 3 PIN block is an 8 byte value that consits of 122 | 123 | - Control field. A 4 bit hex value set to 3. 124 | - PIN length. A 4 bit hex value in the range from 4 to C. 125 | - PIN digits. Each digit is a 4 bit hex value in the range from 0 to 9. 126 | - Random pad character. A 4 bit hex value in the range from A to F. 127 | 128 | The PIN block is then XOR'd by an ANSI PAN block that consists of 129 | 130 | - 4 pad characters. Each is 4 bit hex value set to 0. 131 | - 12 rightmost digits of the PAN excluding the check digit. 132 | 133 | Parameters 134 | ---------- 135 | pin : str 136 | ASCII Personal Identification Number. 137 | pan : str 138 | ASCII Personal Account Number. 139 | 140 | Returns 141 | ------- 142 | pinblock : bytes 143 | Binary 8-byte PIN block. 144 | 145 | Raises 146 | ------ 147 | ValueError 148 | PIN must be between 4 and 12 digits long 149 | PAN must be at least 13 digits long 150 | 151 | Examples 152 | -------- 153 | >>> from psec.pinblock import encode_pinblock_iso_3 154 | >>> encode_pinblock_iso_3("1234", "5544332211009966").hex().upper()[:6] 155 | '341277' 156 | """ 157 | 158 | if len(pin) < 4 or len(pin) > 12 or not _tools.ascii_numeric(pin): 159 | raise ValueError("PIN must be between 4 and 12 digits long") 160 | 161 | if len(pan) < 13 or not _tools.ascii_numeric(pan): 162 | raise ValueError("PAN must be at least 13 digits long") 163 | 164 | random_pad = "".join(_secrets.choice("ABCDEF") for _ in range(10)) 165 | 166 | pinblock = (len(pin) + 48).to_bytes(1, "big") + _binascii.a2b_hex( 167 | pin + random_pad[: 14 - len(pin)] 168 | ) 169 | pan_block = b"\x00\x00" + _binascii.a2b_hex(pan[-13:-1]) 170 | 171 | return _tools.xor(pinblock, pan_block) 172 | 173 | 174 | def encode_pin_field_iso_4(pin: str) -> bytes: 175 | r"""Encode ISO 9564 PIN block format 4 plain text PIN field. 176 | ISO format 4 PIN plain text PIN field is a 16 byte value that consits of 177 | 178 | - Control field. A 4 bit hex value set to 4. 179 | - PIN length. A 4 bit hex value in the range from 4 to C. 180 | - PIN digits. Each digit is a 4 bit hex value in the range from 0 to 9. 181 | - Fill digits. Each digit is a 4 bit hex value set to A. 182 | - Random pad character. A 4 bit hex value in the range from 0 to F. 183 | 184 | Parameters 185 | ---------- 186 | pin : str 187 | ASCII Personal Identification Number. 188 | 189 | Returns 190 | ------- 191 | bytes 192 | Binary 16-byte PIN field block. 193 | 194 | Raises 195 | ------ 196 | ValueError 197 | PIN must be between 4 and 12 digits long. 198 | Padding must be 8 bytes long. 199 | 200 | Examples 201 | -------- 202 | >>> from psec.pinblock import encode_pin_field_iso_4 203 | >>> encode_pin_field_iso_4("1234").hex().upper()[:16] 204 | '441234AAAAAAAAAA' 205 | """ 206 | 207 | if len(pin) < 4 or len(pin) > 12 or not _tools.ascii_numeric(pin): 208 | raise ValueError("PIN must be between 4 and 12 digits long") 209 | 210 | random_pad = _urandom(8).hex().upper() 211 | 212 | pin_len_hex = ( 213 | len(pin).to_bytes(1, "big").hex()[1] 214 | ) # Only the low nibble is relevant, values 4 - C. 215 | pinblock_str = "4" + pin_len_hex + pin + "A" * (14 - len(pin)) + random_pad 216 | return _binascii.a2b_hex(pinblock_str) 217 | 218 | 219 | def encode_pan_field_iso_4(pan: str) -> bytes: 220 | r"""Encode ISO 9564 PIN block format 4 plain text primary account number (PAN) field. 221 | ISO format 4 plain text primary account number field is a 16 byte value that consits of 222 | 223 | - PAN length. A 4 bit hex value in the range from 0 to 7 indicate a PAN length of 12 plus the value of the field 224 | (ranging from dec. 12 to 19). If the PAN is less than 12 digits, the digits are right justified and 225 | padded to the left with zeros and PAN length is set to 0. 226 | - PAN digits. Each digit is a 4 bit hex value in the range from 0 to 9. 227 | - Pad digits. A 4 bit hex value set to 0. 228 | 229 | Parameters 230 | ---------- 231 | pan : str 232 | ASCII Personal Account Number. 233 | 234 | Returns 235 | ------- 236 | bytes 237 | Binary 16-byte PAN field. 238 | 239 | Raises 240 | ------ 241 | ValueError 242 | PAN must be between 1 and 19 digits long. 243 | 244 | Examples 245 | -------- 246 | >>> from psec.pinblock import encode_pan_field_iso_4 247 | >>> encode_pan_field_iso_4("112233445566778899").hex().upper() 248 | '61122334455667788990000000000000' 249 | """ 250 | 251 | if len(pan) < 1 or len(pan) > 19 or not _tools.ascii_numeric(pan): 252 | raise ValueError("PAN must be between 1 and 19 digits long.") 253 | 254 | pan_field = (str(max(0, len(pan) - 12)) + (pan.rjust(12, "0"))).ljust(32, "0") 255 | 256 | return _binascii.a2b_hex(pan_field) 257 | 258 | 259 | def encipher_pinblock_iso_4(key: bytes, pin: str, pan: str) -> bytes: 260 | r"""Encrypt PIN with PAN binding according to ISO 9564 PIN block format 4. ISO format 4 is constructed using two 261 | 16-byte fields of PIN and PAN data respecively which are tied in the encryption process resulting in a 16-byte 262 | enciphered PIN block. 263 | 264 | The following steps are performed: 265 | 266 | - Encode the PIN in the plain text PIN field. 267 | - Encode the PAN in the plain text primary account number (PAN) field. 268 | - Encipher the plain text PIN field with key K. 269 | - Add the resulting intermedate block A modulo-2 (XOR) to the plain text PAN field. 270 | - Encipher the resulting intermediate block B with the same key K. 271 | 272 | Parameters 273 | ---------- 274 | key : bytes 275 | Binary AES key. 276 | pin : str 277 | ASCII Personal Identification Number. 278 | pan : str 279 | ASCII Personal Account Number. 280 | 281 | Returns 282 | ------- 283 | bytes 284 | Binary 16-byte enciphered PIN block. 285 | 286 | Raises 287 | ------ 288 | ValueError 289 | PIN must be between 4 and 12 digits long 290 | Padding must be 8 bytes long. 291 | PAN must be between 1 and 19 digits long. 292 | 293 | Examples 294 | -------- 295 | >>> from psec.pinblock import encipher_pinblock_iso_4 296 | >>> key = bytes.fromhex("00112233445566778899AABBCCDDEEFF") 297 | >>> pin = "1234" 298 | >>> pan = "1234567890123456" 299 | >>> encipher_pinblock_iso_4(key, pin, pan).hex().upper() # doctest: +SKIP 300 | '7CDF645C86CAF763AE34637A66997534' 301 | """ 302 | pin_field = encode_pin_field_iso_4(pin) 303 | pan_field = encode_pan_field_iso_4(pan) 304 | intermediate_block_a = _aes.encrypt_aes_ecb(key, pin_field) 305 | intermediate_block_b = _tools.xor(intermediate_block_a, pan_field) 306 | return _aes.encrypt_aes_ecb(key, intermediate_block_b) 307 | 308 | 309 | def decode_pinblock_iso_0(pinblock: bytes, pan: str) -> str: 310 | r"""Decode ISO 9564 PIN block format 0 aka ANSI PIN block. 311 | ISO format 0 PIN block is an 8 byte value that consits of 312 | 313 | - Control field. A 4 bit hex value set to 0. 314 | - PIN length. A 4 bit hex value in the range from 4 to C. 315 | - PIN digits. Each digit is a 4 bit hex value in the range from 0 to 9. 316 | - Pad character. A 4 bit hex value set to F. 317 | 318 | The PIN block is then XOR'd by an ANSI PAN block that consists of 319 | 320 | - 4 pad characters. Each is 4 bit hex value set to 0. 321 | - 12 rightmost digits of the PAN excluding the check digit. 322 | 323 | Parameters 324 | ---------- 325 | pinblock : bytes 326 | Binary 8-byte PIN block. 327 | pan : str 328 | ASCII Personal Account Number. 329 | 330 | Returns 331 | ------- 332 | pin : str 333 | ASCII Personal Identification Number. 334 | 335 | Raises 336 | ------ 337 | ValueError 338 | PIN block must be 8 bytes long 339 | PIN block must be 16 hexchars long 340 | PIN block is not ISO format 0: control field `X` 341 | PIN block filler is incorrect: `filler` 342 | PIN is not numeric: `pin` 343 | 344 | Examples 345 | -------- 346 | >>> from psec.pinblock import decode_pinblock_iso_0 347 | >>> decode_pinblock_iso_0( 348 | ... bytes.fromhex("041277CDDEEFF669"), 349 | ... "5544332211009966") 350 | '1234' 351 | """ 352 | 353 | if len(pan) < 13 or not _tools.ascii_numeric(pan): 354 | raise ValueError("PAN must be at least 13 digits long") 355 | 356 | if len(pinblock) != 8: 357 | raise ValueError("PIN block must be 8 bytes long") 358 | 359 | pan_block = b"\x00\x00" + _binascii.a2b_hex(pan[-13:-1]) 360 | block = _tools.xor(pinblock, pan_block).hex().upper() 361 | 362 | if block[0] != "0": 363 | raise ValueError(f"PIN block is not ISO format 0: control field `{block[0]}`") 364 | 365 | pin_len = int(block[1], 16) 366 | 367 | if pin_len < 4 or pin_len > 12: 368 | raise ValueError(f"PIN length must be between 4 and 12: `{pin_len}`") 369 | 370 | if block[pin_len + 2 :] != ("F" * (14 - pin_len)): 371 | raise ValueError(f"PIN block filler is incorrect: `{block[pin_len + 2 :]}`") 372 | 373 | pin = block[2 : pin_len + 2] 374 | 375 | if not _tools.ascii_numeric(pin): 376 | raise ValueError(f"PIN is not numeric: `{pin}`") 377 | 378 | return pin 379 | 380 | 381 | def decode_pinblock_iso_2(pinblock: bytes) -> str: 382 | r"""Decode ISO 9564 PIN block format 2. 383 | ISO format 2 PIN block is 8 byte value that consits of 384 | 385 | - Control field. A 4 bit hex value set to 2. 386 | - PIN length. A 4 bit hex value in the range from 4 to C. 387 | - PIN digits. Each digit is a 4 bit hex value in the range from 0 to 9. 388 | - Pad character set to F. 389 | 390 | Parameters 391 | ---------- 392 | pinblock : bytes 393 | Binary 8-byte PIN block. 394 | 395 | Returns 396 | ------- 397 | pin : str 398 | ASCII Personal Identification Number. 399 | 400 | Raises 401 | ------ 402 | ValueError 403 | PIN block must be 8 bytes long 404 | PIN block is not ISO format 2: control field `X` 405 | PIN block filler is incorrect: `filler` 406 | PIN is not numeric: `pin` 407 | 408 | Examples 409 | -------- 410 | >>> from psec.pinblock import decode_pinblock_iso_2 411 | >>> decode_pinblock_iso_2(bytes.fromhex("2C123456789012FF")) 412 | '123456789012' 413 | """ 414 | 415 | if len(pinblock) != 8: 416 | raise ValueError("PIN block must be 8 bytes long") 417 | 418 | block = pinblock.hex().upper() 419 | 420 | if block[0] != "2": 421 | raise ValueError(f"PIN block is not ISO format 2: control field `{block[0]}`") 422 | 423 | pin_len = int(block[1], 16) 424 | 425 | if pin_len < 4 or pin_len > 12: 426 | raise ValueError(f"PIN length must be between 4 and 12: `{pin_len}`") 427 | 428 | if block[pin_len + 2 :] != ("F" * (14 - pin_len)): 429 | raise ValueError(f"PIN block filler is incorrect: `{block[pin_len + 2 :]}`") 430 | 431 | pin = block[2 : pin_len + 2] 432 | 433 | if not _tools.ascii_numeric(pin): 434 | raise ValueError(f"PIN is not numeric: `{pin}`") 435 | 436 | return pin 437 | 438 | 439 | def decode_pinblock_iso_3(pinblock: bytes, pan: str) -> str: 440 | r"""Decode ISO 9564 PIN block format 3. 441 | ISO format 3 PIN block is an 8 byte value that consits of 442 | 443 | - Control field. A 4 bit hex value set to 3. 444 | - PIN length. A 4 bit hex value in the range from 4 to C. 445 | - PIN digits. Each digit is a 4 bit hex value in the range from 0 to 9. 446 | - Random pad character. A 4 bit hex value in the range from A to F. 447 | 448 | The PIN block is then XOR'd by an ANSI PAN block that consists of 449 | 450 | - 4 pad characters. Each is 4 bit hex value set to 0. 451 | - 12 rightmost digits of the PAN excluding the check digit. 452 | 453 | Parameters 454 | ---------- 455 | pinblock : bytes 456 | Binary 8-byte PIN block. 457 | pan : str 458 | ASCII Personal Account Number. 459 | 460 | Returns 461 | ------- 462 | pin : str 463 | ASCII Personal Identification Number. 464 | 465 | Raises 466 | ------ 467 | ValueError 468 | PIN block must be 8 bytes long 469 | PIN block must be 16 hexchars long 470 | PIN block is not ISO format 3: control field `X` 471 | PIN block filler is incorrect: `filler` 472 | PIN is not numeric: `pin` 473 | 474 | Examples 475 | -------- 476 | >>> from psec.pinblock import decode_pinblock_iso_3 477 | >>> decode_pinblock_iso_3( 478 | ... bytes.fromhex("341277EEEFCCB43C"), 479 | ... "5544332211009966") 480 | '1234' 481 | """ 482 | 483 | if len(pan) < 13 or not _tools.ascii_numeric(pan): 484 | raise ValueError("PAN must be at least 13 digits long") 485 | 486 | if len(pinblock) != 8: 487 | raise ValueError("PIN block must be 8 bytes long") 488 | 489 | pan_block = b"\x00\x00" + _binascii.a2b_hex(pan[-13:-1]) 490 | block = _tools.xor(pinblock, pan_block).hex().upper() 491 | 492 | if block[0] != "3": 493 | raise ValueError(f"PIN block is not ISO format 3: control field `{block[0]}`") 494 | 495 | pin_len = int(block[1], 16) 496 | 497 | if pin_len < 4 or pin_len > 12: 498 | raise ValueError(f"PIN length must be between 4 and 12: `{pin_len}`") 499 | 500 | if not set(block[pin_len + 2 :]).issubset(frozenset("ABCDEF")): 501 | raise ValueError(f"PIN block filler is incorrect: `{block[pin_len + 2 :]}`") 502 | 503 | pin = block[2 : pin_len + 2] 504 | 505 | if not _tools.ascii_numeric(pin): 506 | raise ValueError(f"PIN is not numeric: `{pin}`") 507 | 508 | return pin 509 | 510 | 511 | def decode_pin_field_iso_4(pin_field: bytes) -> str: 512 | r"""Decode ISO 9564 PIN block format 4 plain text PIN field. 513 | ISO format 4 PIN plain text PIN field is a 16 byte value that consits of 514 | 515 | - Control field. A 4 bit hex value set to 4. 516 | - PIN length. A 4 bit hex value in the range from 4 to C. 517 | - PIN digits. Each digit is a 4 bit hex value in the range from 0 to 9. 518 | - Fill digits. Each digit is a 4 bit hex value set to A. 519 | - Random pad character. A 4 bit hex value in the range from 0 to F. 520 | 521 | Parameters 522 | ---------- 523 | pin_field : bytes 524 | Binary 16-byte PIN field. 525 | 526 | Returns 527 | ------- 528 | pin : str 529 | ASCII Personal Identification Number. 530 | 531 | Raises 532 | ------ 533 | ValueError 534 | PIN field must be 16 bytes long 535 | PIN field must be 32 hexchars long 536 | PIN field is not ISO format 4: control field `X` 537 | PIN field filler is incorrect: `filler` 538 | PIN length must be between 4 and 12: `pin length` 539 | PIN is not numeric: `pin` 540 | 541 | Examples 542 | -------- 543 | >>> from psec.pinblock import decode_pin_field_iso_4 544 | >>> pin_field = bytes.fromhex("441234AAAAAAAAAA548ED7FD65495950") 545 | >>> decode_pin_field_iso_4(pin_field) 546 | '1234' 547 | """ 548 | 549 | if len(pin_field) != 16: 550 | raise ValueError("PIN field must be 16 bytes long") 551 | 552 | pin_field_str = pin_field.hex().upper() 553 | 554 | if pin_field_str[0] != "4": 555 | raise ValueError( 556 | f"PIN block is not ISO format 4: control field `{pin_field_str[0]}`" 557 | ) 558 | 559 | pin_len = int(pin_field_str[1], 16) 560 | 561 | if pin_len < 4 or pin_len > 12: 562 | raise ValueError(f"PIN length must be between 4 and 12: `{pin_len}`") 563 | 564 | if pin_field_str[pin_len + 2 : 16] != ("A" * (14 - pin_len)): 565 | raise ValueError( 566 | f"PIN block filler is incorrect: `{pin_field_str[pin_len+2: 16]}`" 567 | ) 568 | 569 | pin = pin_field_str[2 : pin_len + 2] 570 | 571 | if not _tools.ascii_numeric(pin): 572 | raise ValueError(f"PIN is not numeric: `{pin}`") 573 | 574 | return pin 575 | 576 | 577 | def decipher_pinblock_iso_4(key: bytes, pin_block: bytes, pan: str) -> str: 578 | r"""Decrypt ISO 9564 PIN block format 4 and extract PIN. 579 | 580 | The following steps are performed: 581 | 582 | - Decipher the PIN block with key K resulting in intermediate block B. 583 | - Encode the PAN in the plain text primary account number (PAN) field. 584 | - Add the intermediate block B modulo-2 (XOR) to the plain text PAN field, resulting in intermediate block A. 585 | - Decipher the intermediate block A with the key K yielding the plain text PIN field. 586 | - Decode the plain text PIN field and extract the PIN. 587 | 588 | Parameters 589 | ---------- 590 | key : bytes 591 | Binary AES key. 592 | pin_block : bytes 593 | Binary 16-byte enciphered PIN block. 594 | 595 | Returns 596 | ------- 597 | pin : str 598 | ASCII Personal Identification Number. 599 | 600 | Raises 601 | ------ 602 | ValueError 603 | Data length must be multiple of AES block size 16. 604 | PAN must be between 1 and 19 digits long. 605 | PIN block is not ISO format 4: control field `X` 606 | PIN block filler is incorrect: `filler` 607 | PIN length must be between 4 and 12: `pin length`" 608 | PIN is not numeric: `pin` 609 | 610 | 611 | Examples 612 | -------- 613 | >>> from psec.pinblock import decipher_pinblock_iso_4 614 | >>> key = bytes.fromhex("00112233445566778899AABBCCDDEEFF") 615 | >>> pan = "1234567890123456" 616 | >>> pin_block = bytes.fromhex("E4BE5B623AF7E006AC319E5B93544564") 617 | >>> decipher_pinblock_iso_4(key, pin_block, pan) 618 | '1234' 619 | """ 620 | 621 | intermediate_block_b = _aes.decrypt_aes_ecb(key, pin_block) 622 | pan_field = encode_pan_field_iso_4(pan) 623 | intermediate_block_a = _tools.xor(intermediate_block_b, pan_field) 624 | pin_field = _aes.decrypt_aes_ecb(key, intermediate_block_a) 625 | return decode_pin_field_iso_4(pin_field) 626 | -------------------------------------------------------------------------------- /tests/test_tr31.py: -------------------------------------------------------------------------------- 1 | from os import urandom 2 | import pytest 3 | from psec import tr31 4 | 5 | 6 | def test_header_load() -> None: 7 | h = tr31.Header() 8 | assert h.load("B0000P0TE00N0000xxxxxxxx") == 16 9 | assert h.version_id == "B" 10 | assert h.key_usage == "P0" 11 | assert h.algorithm == "T" 12 | assert h.mode_of_use == "E" 13 | assert h.exportability == "N" 14 | assert h.reserved == "00" 15 | assert len(h.blocks) == 0 16 | assert str(h) == "B0016P0TE00N0000" 17 | 18 | 19 | def test_header_blocks_dict() -> None: 20 | h = tr31.Header("B", "P0", "T", "E") 21 | h.blocks["KS"] = "ABCD" 22 | assert len(h.blocks) == 1 23 | assert h.blocks["KS"] == "ABCD" 24 | assert ("KS" in h.blocks) is True 25 | assert repr(h.blocks) == "{'KS': 'ABCD'}" 26 | 27 | del h.blocks["KS"] 28 | assert len(h.blocks) == 0 29 | with pytest.raises(KeyError): 30 | h.blocks["KS"] 31 | assert ("KS" in h.blocks) is False 32 | assert repr(h.blocks) == "{}" 33 | 34 | 35 | def test_header_load_optional_des() -> None: 36 | h = tr31.Header() 37 | assert h.load("B0000P0TE00N0100KS1800604B120F9292800000xxxxxxxx") == 40 38 | assert h.version_id == "B" 39 | assert h.key_usage == "P0" 40 | assert h.algorithm == "T" 41 | assert h.mode_of_use == "E" 42 | assert h.exportability == "N" 43 | assert h.reserved == "00" 44 | assert len(h.blocks) == 1 45 | assert h.blocks["KS"] == "00604B120F9292800000" 46 | assert str(h) == "B0040P0TE00N0100KS1800604B120F9292800000" 47 | 48 | 49 | def test_header_load_optional_aes() -> None: 50 | h = tr31.Header() 51 | assert h.load("D0000P0TE00N0100KS1800604B120F9292800000xxxxxxxx") == 40 52 | assert h.version_id == "D" 53 | assert h.key_usage == "P0" 54 | assert h.algorithm == "T" 55 | assert h.mode_of_use == "E" 56 | assert h.exportability == "N" 57 | assert h.reserved == "00" 58 | assert len(h.blocks) == 1 59 | assert h.blocks["KS"] == "00604B120F9292800000" 60 | assert str(h) == "D0048P0TE00N0200KS1800604B120F9292800000PB080000" 61 | 62 | 63 | def test_header_load_optional_with_bad_count_des() -> None: 64 | """One optional block is present, but number of optional blocks is 00. 65 | Output header must be multiple of 8.""" 66 | h = tr31.Header() 67 | assert h.load("B0000P0TE00N0000KS1800604B120F9292800000") == 16 68 | assert h.version_id == "B" 69 | assert h.key_usage == "P0" 70 | assert h.algorithm == "T" 71 | assert h.mode_of_use == "E" 72 | assert h.exportability == "N" 73 | assert h.reserved == "00" 74 | assert len(h.blocks) == 0 75 | assert str(h) == "B0016P0TE00N0000" 76 | 77 | 78 | def test_header_load_optional_with_bad_count_aes() -> None: 79 | """One optional block is present, but number of optional blocks is 00. 80 | Output header must be multiple of 16.""" 81 | h = tr31.Header() 82 | assert h.load("D0000P0TE00N0000KS1800604B120F9292800000") == 16 83 | assert h.version_id == "D" 84 | assert h.key_usage == "P0" 85 | assert h.algorithm == "T" 86 | assert h.mode_of_use == "E" 87 | assert h.exportability == "N" 88 | assert h.reserved == "00" 89 | assert len(h.blocks) == 0 90 | assert str(h) == "D0016P0TE00N0000" 91 | 92 | 93 | def test_header_load_optional_padded_des() -> None: 94 | """Two optional blocks are present, one is pad block. Output header must be multiple of 8.""" 95 | h = tr31.Header() 96 | assert h.load("B0000P0TE00N0200KS1200604B120F9292PB0600") == 40 97 | assert h.version_id == "B" 98 | assert h.key_usage == "P0" 99 | assert h.algorithm == "T" 100 | assert h.mode_of_use == "E" 101 | assert h.exportability == "N" 102 | assert h.reserved == "00" 103 | assert len(h.blocks) == 1 104 | assert h.blocks["KS"] == "00604B120F9292" 105 | assert str(h) == "B0040P0TE00N0200KS1200604B120F9292PB0600" 106 | 107 | 108 | def test_header_load_optional_padded_aes() -> None: 109 | """Two optional blocks are present, one is pad block. Output header must be multiple of 16.""" 110 | h = tr31.Header() 111 | assert h.load("D0000P0TE00N0200KS1200604B120F9292PB0600") == 40 112 | assert h.version_id == "D" 113 | assert h.key_usage == "P0" 114 | assert h.algorithm == "T" 115 | assert h.mode_of_use == "E" 116 | assert h.exportability == "N" 117 | assert h.reserved == "00" 118 | assert len(h.blocks) == 1 119 | assert h.blocks["KS"] == "00604B120F9292" 120 | assert str(h) == "D0048P0TE00N0200KS1200604B120F9292PB0E0000000000" 121 | 122 | 123 | def test_header_load_optional_256_des() -> None: 124 | """An optional block with length >255. Output header must be multiple of 8.""" 125 | h = tr31.Header() 126 | assert h.load("B0000P0TE00N0200KS0002010A" + "P" * 256 + "PB0600") == 288 127 | assert h.version_id == "B" 128 | assert h.key_usage == "P0" 129 | assert h.algorithm == "T" 130 | assert h.mode_of_use == "E" 131 | assert h.exportability == "N" 132 | assert h.reserved == "00" 133 | assert len(h.blocks) == 1 134 | assert h.blocks["KS"] == "P" * 256 135 | assert str(h) == "B0288P0TE00N0200KS0002010A" + "P" * 256 + "PB0600" 136 | 137 | 138 | def test_header_load_optional_256_aes() -> None: 139 | """An optional block with length >255. Output header must be multiple of 16.""" 140 | h = tr31.Header() 141 | assert h.load("D0000P0TE00N0200KS0002010A" + "P" * 256 + "PB0600") == 288 142 | assert h.version_id == "D" 143 | assert h.key_usage == "P0" 144 | assert h.algorithm == "T" 145 | assert h.mode_of_use == "E" 146 | assert h.exportability == "N" 147 | assert h.reserved == "00" 148 | assert len(h.blocks) == 1 149 | assert h.blocks["KS"] == "P" * 256 150 | assert str(h) == "D0288P0TE00N0200KS0002010A" + "P" * 256 + "PB0600" 151 | 152 | 153 | def test_header_load_optional_extended_length_des() -> None: 154 | """An optional block with extended length where it's not necessary. 155 | Output header must be multiple of 8.""" 156 | h = tr31.Header() 157 | assert h.load("B0000P0TE00N0200KS00011600604B120F9292PB0A000000") == 48 158 | assert h.version_id == "B" 159 | assert h.key_usage == "P0" 160 | assert h.algorithm == "T" 161 | assert h.mode_of_use == "E" 162 | assert h.exportability == "N" 163 | assert h.reserved == "00" 164 | assert len(h.blocks) == 1 165 | assert h.blocks["KS"] == "00604B120F9292" 166 | assert str(h) == "B0040P0TE00N0200KS1200604B120F9292PB0600" 167 | 168 | 169 | def test_header_load_optional_extended_length_aes() -> None: 170 | """An optional block with extended length where it's not necessary. 171 | Output header must be multiple of 16.""" 172 | h = tr31.Header() 173 | assert h.load("D0000P0TE00N0200KS00011600604B120F9292PB0A000000") == 48 174 | assert h.version_id == "D" 175 | assert h.key_usage == "P0" 176 | assert h.algorithm == "T" 177 | assert h.mode_of_use == "E" 178 | assert h.exportability == "N" 179 | assert h.reserved == "00" 180 | assert len(h.blocks) == 1 181 | assert h.blocks["KS"] == "00604B120F9292" 182 | assert str(h) == "D0048P0TE00N0200KS1200604B120F9292PB0E0000000000" 183 | 184 | 185 | def test_header_load_optional_multiple_des() -> None: 186 | """Load multiple optional blocks. 187 | Output header must be multiple of 8.""" 188 | h = tr31.Header() 189 | assert h.load("B0000P0TE00N0400KS1800604B120F9292800000T104T20600PB0600") == 56 190 | assert h.version_id == "B" 191 | assert h.key_usage == "P0" 192 | assert h.algorithm == "T" 193 | assert h.mode_of_use == "E" 194 | assert h.exportability == "N" 195 | assert h.reserved == "00" 196 | assert len(h.blocks) == 3 197 | assert h.blocks["KS"] == "00604B120F9292800000" 198 | assert h.blocks["T1"] == "" 199 | assert h.blocks["T2"] == "00" 200 | assert str(h) == "B0056P0TE00N0400KS1800604B120F9292800000T104T20600PB0600" 201 | 202 | 203 | def test_header_load_optional_multiple_aes() -> None: 204 | """Load multiple optional blocks. 205 | Output header must be multiple of 16.""" 206 | h = tr31.Header() 207 | assert h.load("D0000P0TE00N0400KS1800604B120F9292800000T104T20600PB0600") == 56 208 | assert h.version_id == "D" 209 | assert h.key_usage == "P0" 210 | assert h.algorithm == "T" 211 | assert h.mode_of_use == "E" 212 | assert h.exportability == "N" 213 | assert h.reserved == "00" 214 | assert len(h.blocks) == 3 215 | assert h.blocks["KS"] == "00604B120F9292800000" 216 | assert h.blocks["T1"] == "" 217 | assert h.blocks["T2"] == "00" 218 | assert str(h) == "D0064P0TE00N0400KS1800604B120F9292800000T104T20600PB0E0000000000" 219 | 220 | 221 | def test_header_load_optional_reset() -> None: 222 | """Make sure optional blocks are reset between loads""" 223 | h = tr31.Header() 224 | assert h.load("B0000P0TE00N0400KS1800604B120F9292800000T104T20600PB0600") == 56 225 | assert h.load("B0000P0TE00N0000") == 16 226 | assert h.version_id == "B" 227 | assert h.key_usage == "P0" 228 | assert h.algorithm == "T" 229 | assert h.mode_of_use == "E" 230 | assert h.exportability == "N" 231 | assert h.reserved == "00" 232 | assert len(h.blocks) == 0 233 | assert str(h) == "B0016P0TE00N0000" 234 | 235 | 236 | # fmt: off 237 | @pytest.mark.parametrize( 238 | ["header", "error"], 239 | [ 240 | ("B0000P0TE00N0100", "Block ID () is malformed."), 241 | ("B0000P0TE00N0100K", "Block ID (K) is malformed."), 242 | ("B0000P0TE00N0100KS", "Block KS length () is malformed. Expecting 2 hexchars."), 243 | ("B0000P0TE00N0100KS1", "Block KS length (1) is malformed. Expecting 2 hexchars."), 244 | ("B0000P0TE00N0100KS1Y", "Block KS length (1Y) is malformed. Expecting 2 hexchars."), 245 | ("B0000P0TE00N0100KS02", "Block KS length does not include block ID and length."), 246 | ("B0000P0TE00N0100KS071", "Block KS data is malformed. Received 1/3. Block data: '1'"), 247 | ("B0000P0TE00N0100KS00", "Block KS length of length () is malformed. Expecting 2 hexchars."), 248 | ("B0000P0TE00N0100KS001", "Block KS length of length (1) is malformed. Expecting 2 hexchars."), 249 | ("B0000P0TE00N0100KS001S", "Block KS length of length (1S) is malformed. Expecting 2 hexchars."), 250 | ("B0000P0TE00N0100KS0000", "Block KS length of length must not be 0."), 251 | ("B0000P0TE00N0100KS0001", "Block KS length () is malformed. Expecting 2 hexchars."), 252 | ("B0000P0TE00N0100KS00010", "Block KS length (0) is malformed. Expecting 2 hexchars."), 253 | ("B0000P0TE00N0100KS00010H", "Block KS length (0H) is malformed. Expecting 2 hexchars."), 254 | ("B0000P0TE00N0100KS000101", "Block KS length does not include block ID and length."), 255 | ("B0000P0TE00N0100KS0001FF", "Block KS data is malformed. Received 0/247. Block data: ''"), 256 | ("B0000P0TE00N0200KS07000T", "Block ID (T) is malformed."), 257 | ("B0000P0TE00N0200KS0600TT", "Block TT length () is malformed. Expecting 2 hexchars."), 258 | ("B0000P0TE00N0200KS050TT1", "Block TT length (1) is malformed. Expecting 2 hexchars."), 259 | ("B0000P0TE00N0200KS04TT1X", "Block TT length (1X) is malformed. Expecting 2 hexchars."), 260 | ("B0000P0TE00N0200KS04TT03", "Block TT length does not include block ID and length."), 261 | ("B0000P0TE00N0200KS04TT05", "Block TT data is malformed. Received 0/1. Block data: ''"), 262 | ("B0000P0TE00N0200KS04TT00", "Block TT length of length () is malformed. Expecting 2 hexchars."), 263 | ("B0000P0TE00N0200KS04TT001", "Block TT length of length (1) is malformed. Expecting 2 hexchars."), 264 | ("B0000P0TE00N0200KS04TT001S", "Block TT length of length (1S) is malformed. Expecting 2 hexchars."), 265 | ("B0000P0TE00N0200KS04TT0000", "Block TT length of length must not be 0."), 266 | ("B0000P0TE00N0200KS04TT0001", "Block TT length () is malformed. Expecting 2 hexchars."), 267 | ("B0000P0TE00N0200KS04TT00010", "Block TT length (0) is malformed. Expecting 2 hexchars."), 268 | ("B0000P0TE00N0200KS04TT00010H", "Block TT length (0H) is malformed. Expecting 2 hexchars."), 269 | ("B0000P0TE00N0200KS04TT000101", "Block TT length does not include block ID and length."), 270 | ("B0000P0TE00N0200KS04TT00011F", "Block TT data is malformed. Received 0/23. Block data: ''"), 271 | ("B0000P0TE00N0100**04", "Block ID (**) is invalid. Expecting 2 alphanumeric characters."), 272 | ("B0000P0TE00N0200KS0600??04", "Block ID (??) is invalid. Expecting 2 alphanumeric characters."), 273 | ("B0000P0TE00N0100KS05\x03", "Block KS data is invalid. Expecting ASCII printable characters. Data: '\x03'"), 274 | ("B0000P0TE00N0200KS04TT05\xFF", "Block TT data is invalid. Expecting ASCII printable characters. Data: '\xFF'"), 275 | ], 276 | ) 277 | # fmt: on 278 | def test_header_block_load_exceptions(header: str, error: str) -> None: 279 | """Make sure optional blocks handle input validation""" 280 | h = tr31.Header() 281 | with pytest.raises(tr31.HeaderError) as e: 282 | h.load(header) 283 | assert e.value.args[0] == error 284 | 285 | 286 | def test_header_block_dump_exception_block_too_large() -> None: 287 | """Make sure optional blocks handle a single large block 288 | that's too large to fit into a key block.""" 289 | h = tr31.Header() 290 | h.blocks["LG"] = "P" * 70000 # > unsigned int 16 291 | with pytest.raises(tr31.HeaderError) as e: 292 | _ = h.dump(16) 293 | assert e.value.args[0] == "Block LG length (70000) is too long." 294 | 295 | 296 | def test_header_block_dump_exception_too_many_blocks() -> None: 297 | """Make sure optional blocks handle a limit of 99 blocks 298 | without a pad block embedded.""" 299 | h = tr31.Header() 300 | for i in range(0, 100): 301 | h.blocks[str(i).zfill(2)] = "PPPP" # each block is multiple of 8 302 | with pytest.raises(tr31.HeaderError) as e: 303 | _ = h.dump(16) 304 | assert e.value.args[0] == "Number of blocks (100) exceeds limit of 99." 305 | 306 | 307 | def test_header_block_dump_exception_too_many_blocks_pb() -> None: 308 | """Make sure optional blocks handle a limit of 99 blocks 309 | with a pad block embedded.""" 310 | h = tr31.Header() 311 | for i in range(0, 99): 312 | h.blocks[str(i).zfill(2)] = "PP" 313 | with pytest.raises(tr31.HeaderError) as e: 314 | _ = h.dump(16) 315 | assert e.value.args[0] == "Number of blocks (100) exceeds limit of 99." 316 | 317 | 318 | # fmt: off 319 | @pytest.mark.parametrize( 320 | ["header", "error"], 321 | [ 322 | ("_0000P0TE00N0000", "Header must be ASCII alphanumeric. Header: '_0000P0TE00N0000'"), 323 | ("", "Header length (0) must be >=16. Header: ''"), 324 | ("B0000", "Header length (5) must be >=16. Header: 'B0000'"), 325 | ("B0000P0TE00N0X00", "Number of blocks (0X) is invalid. Expecting 2 digits."), 326 | ], 327 | ) 328 | # fmt: on 329 | def test_header_load_exceptions(header: str, error: str) -> None: 330 | """Make sure header handle load method validation""" 331 | h = tr31.Header() 332 | with pytest.raises(tr31.HeaderError) as e: 333 | h.load(header) 334 | assert e.value.args[0] == error 335 | 336 | 337 | # fmt: off 338 | @pytest.mark.parametrize( 339 | ["version_id", "key_usage", "algorithm", "mode_of_use", "version_num", "exportability", "error"], 340 | [ 341 | ("_", "P0", "T", "E", "00", "N", "Version ID (_) is not supported."), 342 | ("B0", "P0", "T", "E", "00", "N", "Version ID (B0) is not supported."), 343 | ("", "P0", "T", "E", "00", "N", "Version ID () is not supported."), 344 | ("B", "P_", "T", "E", "00", "N", "Key usage (P_) is invalid."), 345 | ("B", "P", "T", "E", "00", "N", "Key usage (P) is invalid."), 346 | ("B", "P00", "T", "E", "00", "N", "Key usage (P00) is invalid."), 347 | ("B", "P0", "", "E", "00", "N", "Algorithm () is invalid."), 348 | ("B", "P0", "_", "E", "00", "N", "Algorithm (_) is invalid."), 349 | ("B", "P0", "T0", "E", "00", "N", "Algorithm (T0) is invalid."), 350 | ("B", "P0", "T", "_", "00", "N", "Mode of use (_) is invalid."), 351 | ("B", "P0", "T", "", "00", "N", "Mode of use () is invalid."), 352 | ("B", "P0", "T", "EE", "00", "N", "Mode of use (EE) is invalid."), 353 | ("B", "P0", "T", "E", "0", "N", "Version number (0) is invalid."), 354 | ("B", "P0", "T", "E", "000", "N", "Version number (000) is invalid."), 355 | ("B", "P0", "T", "E", "0_", "N", "Version number (0_) is invalid."), 356 | ("B", "P0", "T", "E", "00", "", "Exportability () is invalid."), 357 | ("B", "P0", "T", "E", "00", "NN", "Exportability (NN) is invalid."), 358 | ("B", "P0", "T", "E", "00", "_", "Exportability (_) is invalid."), 359 | ], 360 | ) 361 | # fmt: on 362 | def test_header_attributes_exceptions( 363 | version_id: str, 364 | key_usage: str, 365 | algorithm: str, 366 | mode_of_use: str, 367 | version_num: str, 368 | exportability: str, 369 | error: str, 370 | ) -> None: 371 | """Make sure header handle attribute validation""" 372 | with pytest.raises(tr31.HeaderError) as e: 373 | _ = tr31.Header( 374 | version_id, key_usage, algorithm, mode_of_use, version_num, exportability 375 | ) 376 | assert e.value.args[0] == error 377 | 378 | 379 | def test_header_dump_exception_kb_too_large() -> None: 380 | """Make sure header dump method handle input validation: header size too large""" 381 | h = tr31.Header() 382 | h.blocks["T0"] = "P" * 9990 383 | with pytest.raises(tr31.HeaderError) as e: 384 | _ = h.dump(16) 385 | assert e.value.args[0] == "Total key block length (10080) exceeds limit of 9999." 386 | 387 | 388 | # fmt: off 389 | @pytest.mark.parametrize( 390 | ["version_id", "kbpk"], 391 | [ 392 | ("A", b"A"*8+b"B"*8+b"C"*8), 393 | ("A", b"A"*8+b"B"*8), 394 | ("A", b"A"*8), 395 | ("B", b"A"*8+b"B"*8+b"C"*8), 396 | ("B", b"A"*8+b"B"*8), 397 | ("C", b"A"*8+b"B"*8+b"C"*8), 398 | ("C", b"A"*8+b"B"*8), 399 | ("C", b"A"*8), 400 | ("D", b"A"*16+b"B"*8+b"C"*8), 401 | ("D", b"A"*16+b"B"*8), 402 | ("D", b"A"*16), 403 | ], 404 | ) 405 | # fmt: on 406 | def test_kb_sanity(version_id: str, kbpk: bytes) -> None: 407 | """Make sure that wrapping and then unwrapping produces original key""" 408 | 409 | def sanity_check(kbpk: bytes, key: bytes, header: tr31.Header) -> None: 410 | kb = tr31.KeyBlock(kbpk, header) 411 | raw_kb = kb.wrap(key) 412 | 413 | assert key == kb.unwrap(raw_kb) 414 | assert key == tr31.KeyBlock(kbpk).unwrap(raw_kb) 415 | 416 | header = tr31.Header(version_id, "P0", "T", "E", "00", "N") 417 | 418 | # It does not matter what the key is. Or even if it's a valid key for the algorithm. 419 | for key_len in [0, 1, 8, 16, 24, 32, 99, 999]: 420 | sanity_check(kbpk, urandom(key_len), header) 421 | 422 | 423 | # fmt: off 424 | @pytest.mark.parametrize( 425 | ["kbpk", "key", "kb"], 426 | [ 427 | ("AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB", "EEEEEEEEEEEEEEEE", "A0088M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF354468910379AA5BBA6"), 428 | ("AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB", "EEEEEEEEEEEEEEEE", "B0096M3TC00E0000B6CD513680EF255FC0DC590726FD0129A7CF6602E7F271631AB4EE7350642F11181AF4CC12F12FD9"), 429 | ("AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB", "EEEEEEEEEEEEEEEE", "C0088M3TC00E0000A53CF172FE6562E7FDD5E6482E8925DA46F7FFE4D1BAD49EB33A9EDBB96A8A8D39F13A31"), 430 | ("AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB", "CCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDD", "A0088M3TC00E0000BE8AE894906D0B8F6FF555573A3907DC37FF13B12CE1CB8A97A97C8414AE1A8FF9183122"), 431 | ("AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB", "CCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDD", "B0096M3TC00E0000D578DACC2286C7D10F20DEA88799CA8A2F44E0CC21226A2158D5DC8FD5C78E621327DA956C678808"), 432 | ("AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB", "CCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDD", "C0088M3TC00E00009BC6306FC31891BF87B3148463627B1D68C603D9FAB9074E4A0D2E78D40B29905A826F5C"), 433 | ("AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCC", "CCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDD", "A0088M3TC00E000022BD7EC46BBE2A6A73389D1BA6DB63120B386F912839F4679C0523399E4D8D0F1D9A356E"), 434 | ("AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCC", "CCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDD", "B0096M3TC00E0000C7C6FE86A5DE769C20DCA238C52341378B484D544A9764D43963C3B2824AE56C2D07A565DD3AB342"), 435 | ("AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCC", "CCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDD", "C0088M3TC00E000091FA4978279FD9C218BDCBE9CC62F11A182F828406B67AC61B5573748FCF348FD59FA93A"), 436 | ("89E88CF7931444F334BD7547FC3F380C", "F039121BEC83D26B169BDCD5B22AAF8F", "A0072P0TE00E0000F5161ED902807AF26F1D62263644BD24192FDB3193C730301CEE8701"), 437 | ("DD7515F2BFC17F85CE48F3CA25CB21F6", "3F419E1CB7079442AA37474C2EFBF8B8", "B0080P0TE00E000094B420079CC80BA3461F86FE26EFC4A3B8E4FA4C5F5341176EED7B727B8A248E"), 438 | ("B8ED59E0A279A295E9F5ED7944FD06B9", "EDB380DD340BC2620247D445F5B8D678", "C0096B0TX12S0100KS1800604B120F9292800000BFB9B689CB567E66FC3FEE5AD5F52161FC6545B9D60989015D02155C"), 439 | ("1D22BF32387C600AD97F9B97A51311AC", "E8BC63E5479455E26577F715D587FE68", "B0104B0TX12S0100KS1800604B120F9292800000BB68BE8680A400D9191AD4ECE45B6E6C0D21C4738A52190E248719E24B433627"), 440 | ("89E88CF7931444F334BD7547FC3F380C", "F039121BEC83D26B169BDCD5B22AAF8F", "A0088P0TE00E00007DD4DD9566DC0E2F956DCAC0FDE9153159539373E9D82D3CD4AFD305A7EF1BA67FE03712"), 441 | ("89E88CF7931444F334BD7547FC3F380C", "F039121BEC83D26B169BDCD5B22AAF8F", "B0120P0TE12E0100KS1800604B120F9292800000E6E28F097CB0350B2EB2DF520947F779FA34D9759CEE7E0DEEACF8353DB778D47FA4EC20DA3A9754"), 442 | ("B8ED59E0A279A295E9F5ED7944FD06B9", "F039121BEC83D26B169BDCD5B22AAF8F", "A0112P0TE12E0200KS1400604B120F929280PB047A1BB737854CD7AF58A8A1E4506A942277EDA76EBA6BA228AF62ADDA3AD8799E8B2C8CD7"), 443 | ("89E88CF7931444F334BD7547FC3F380C", "F039121BEC83D26B169BDCD5B22AAF8F", "A0088P0TE00E00007DD4DD9566DC0E2F956DCAC0FDE91531723FD88F18DE071A57189B4D3C483341ED79F4E0"), 444 | ("88E1AB2A2E3DD38C1FA039A536500CC8A87AB9D62DC92C01058FA79F44657DE6", "3F419E1CB7079442AA37474C2EFBF8B8", "D0112P0AE00E0000B82679114F470F540165EDFBF7E250FCEA43F810D215F8D207E2E417C07156A27E8E31DA05F7425509593D03A457DC34"), 445 | ], 446 | ) 447 | # fmt: on 448 | def test_kb_known_values(kbpk: str, key: str, kb: str) -> None: 449 | """Test against known values from 3rd parties""" 450 | kbpk_b = bytes.fromhex(kbpk) 451 | key_b = bytes.fromhex(key) 452 | assert key_b == tr31.KeyBlock(kbpk_b).unwrap(kb) 453 | 454 | 455 | # fmt: off 456 | @pytest.mark.parametrize( 457 | ["version_id", "algorithm", "key_len", "masked_key_len", "kb_len"], 458 | [ 459 | ("A", "D", 24, 24, 88), 460 | ("A", "D", 16, 24, 88), 461 | ("A", "D", 8, 24, 88), 462 | ("A", "D", 24, None, 88), 463 | ("A", "D", 16, None, 88), 464 | ("A", "D", 8, None, 88), 465 | ("A", "D", 16, 16, 72), 466 | ("A", "D", 16, 8, 72), 467 | ("A", "D", 16, 0, 72), 468 | ("A", "D", 16, -8, 72), 469 | ("A", "D", 8, 8, 56), 470 | ("A", "D", 8, 0, 56), 471 | ("B", "T", 24, 24, 96), 472 | ("B", "T", 16, 24, 96), 473 | ("B", "T", 8, 24, 96), 474 | ("B", "T", 24, None, 96), 475 | ("B", "T", 16, None, 96), 476 | ("B", "T", 8, None, 96), 477 | ("B", "T", 16, 16, 80), 478 | ("B", "T", 16, 8, 80), 479 | ("B", "T", 16, 0, 80), 480 | ("B", "T", 16, -8, 80), 481 | ("B", "T", 8, 8, 64), 482 | ("B", "T", 8, 0, 64), 483 | ("C", "T", 24, 24, 88), 484 | ("C", "T", 16, 24, 88), 485 | ("C", "T", 8, 24, 88), 486 | ("C", "T", 24, None, 88), 487 | ("C", "T", 16, None, 88), 488 | ("C", "T", 8, None, 88), 489 | ("C", "T", 16, 16, 72), 490 | ("C", "T", 16, 8, 72), 491 | ("C", "T", 16, 0, 72), 492 | ("C", "T", 16, -8, 72), 493 | ("C", "T", 8, 8, 56), 494 | ("C", "T", 8, 0, 56), 495 | ("D", "A", 32, 32, 144), 496 | ("D", "A", 24, 32, 144), 497 | ("D", "A", 16, 32, 144), 498 | ("D", "A", 32, None, 144), 499 | ("D", "A", 24, None, 144), 500 | ("D", "A", 16, None, 144), 501 | ("D", "A", 24, 24, 112), 502 | ("D", "A", 24, 16, 112), 503 | ("D", "A", 24, 8, 112), 504 | ("D", "A", 24, 0, 112), 505 | ("D", "A", 24, -1, 112), 506 | ("D", "A", 16, 16, 112), 507 | ("D", "A", 16, 8, 112), 508 | ("D", "A", 16, 0, 112), 509 | ("D", "A", 16, -1, 112), 510 | ("D", "T", 24, 24, 112), 511 | ("D", "T", 16, 24, 112), 512 | ("D", "T", 8, 24, 112), 513 | ("D", "T", 24, None, 112), 514 | ("D", "T", 16, None, 112), 515 | ("D", "T", 8, None, 112), 516 | ("D", "T", 16, 16, 112), 517 | ("D", "T", 16, 8, 112), 518 | ("D", "T", 16, 0, 112), 519 | ("D", "T", 16, -8, 112), 520 | ("D", "T", 8, 8, 80), 521 | ("D", "T", 8, 0, 80), 522 | ], 523 | ) 524 | # fmt: on 525 | def test_kb_masking_key_length( 526 | version_id: str, algorithm: str, key_len: int, masked_key_len: int, kb_len: int 527 | ) -> None: 528 | """Test KB key masking""" 529 | kb = tr31.KeyBlock(b"E" * 24) 530 | kb.header.version_id = version_id 531 | kb.header.algorithm = algorithm 532 | kb_s = kb.wrap(b"F" * key_len, masked_key_len) 533 | assert len(kb_s) == kb_len 534 | assert kb_s[1:5] == str(kb_len).zfill(4) 535 | 536 | 537 | def test_kb_wrap_unsupported_kb_version() -> None: 538 | """Test wrap with unsupported version ID""" 539 | with pytest.raises(tr31.KeyBlockError) as e: 540 | kb = tr31.KeyBlock(b"E" * 16) 541 | kb.header._version_id = "X" # have to do this to bypass edit checks 542 | _ = kb.wrap(b"E" * 8) 543 | assert e.value.args[0] == "Key block version ID (X) is not supported." 544 | 545 | 546 | # fmt: off 547 | @pytest.mark.parametrize( 548 | ["version_id", "kbpk_len", "key_len", "error"], 549 | [ 550 | ("A", 7, 24, "KBPK length (7) must be Single, Double or Triple DES for key block version A."), 551 | ("B", 7, 24, "KBPK length (7) must be Double or Triple DES for key block version B."), 552 | ("C", 7, 24, "KBPK length (7) must be Single, Double or Triple DES for key block version C."), 553 | ("D", 17, 24, "KBPK length (17) must be AES-128, AES-192 or AES-256 for key block version D."), 554 | ], 555 | ) 556 | # fmt: on 557 | def test_kb_wrap_exceptions( 558 | version_id: str, kbpk_len: int, key_len: int, error: str 559 | ) -> None: 560 | """Test wrap exceptions""" 561 | with pytest.raises(tr31.KeyBlockError) as e: 562 | kb = tr31.KeyBlock(b"E" * kbpk_len) 563 | kb.header._version_id = version_id 564 | _ = kb.wrap(b"F" * key_len) 565 | assert e.value.args[0] == error 566 | 567 | 568 | def test_kb_init_with_raw_header() -> None: 569 | """Initialize KB with raw TR-31 header string""" 570 | kb = tr31.KeyBlock(b"E" * 16, "B0000P0TE00N0000xxxxxxxx") 571 | assert kb.header.version_id == "B" 572 | assert kb.header.key_usage == "P0" 573 | assert kb.header.algorithm == "T" 574 | assert kb.header.mode_of_use == "E" 575 | assert kb.header.exportability == "N" 576 | assert kb.header.reserved == "00" 577 | assert len(kb.header.blocks) == 0 578 | assert str(kb) == "B0016P0TE00N0000" 579 | 580 | 581 | def test_kb_init_with_raw_header_blocks() -> None: 582 | """Initialize KB with raw TR-31 header string""" 583 | kb = tr31.KeyBlock(b"E" * 16, "B0000P0TE00N0100KS1800604B120F9292800000xxxxxxxx") 584 | assert kb.header.version_id == "B" 585 | assert kb.header.key_usage == "P0" 586 | assert kb.header.algorithm == "T" 587 | assert kb.header.mode_of_use == "E" 588 | assert kb.header.exportability == "N" 589 | assert kb.header.reserved == "00" 590 | assert len(kb.header.blocks) == 1 591 | assert kb.header.blocks["KS"] == "00604B120F9292800000" 592 | assert str(kb) == "B0040P0TE00N0100KS1800604B120F9292800000" 593 | 594 | 595 | # fmt: off 596 | @pytest.mark.parametrize( 597 | ["kbpk_len", "kb", "error"], 598 | [ 599 | (16, "B0040P0TE00N0000", "Key block header length (40) doesn't match input data length (16)."), 600 | (16, "BX040P0TE00N0000", "Key block header length (X040) is malformed. Expecting 4 digits."), 601 | 602 | (16, "A0087M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF354468910379AA5BBA", "Key block length (87) must be multiple of 8 for key block version A."), 603 | (16, "B0087M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF354468910379AA5BBA", "Key block length (87) must be multiple of 8 for key block version B."), 604 | (16, "C0087M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF354468910379AA5BBA", "Key block length (87) must be multiple of 8 for key block version C."), 605 | (16, "D0087M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF354468910379AA5BBA", "Key block length (87) must be multiple of 16 for key block version D."), 606 | 607 | (16, "A0088M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF354468910379AA5BBAX", "Key block MAC must be valid hexchars. MAC: '9AA5BBAX'"), 608 | (16, "B0088M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF354468910379AA5BBAX", "Key block MAC must be valid hexchars. MAC: '468910379AA5BBAX'"), 609 | (16, "C0088M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF354468910379AA5BBAX", "Key block MAC must be valid hexchars. MAC: '9AA5BBAX'"), 610 | (16, "D0112P0AE00E0000DDF7B73888F22B757600010215895621B94A4E8DA57DD3E01BB66FF046A4E6B89B8F5C30BDD3A946205FDF791C3548EX", "Key block MAC must be valid hexchars. MAC: '9B8F5C30BDD3A946205FDF791C3548EX'"), 611 | 612 | (16, "A0024M3TC00E0100TT04BBA6", "Key block MAC is malformed. Received 4 bytes MAC. Expecting 8 bytes for key block version A. MAC: 'BBA6'"), 613 | (16, "B0024M3TC00E00009AA5BBA6", "Key block MAC is malformed. Received 8 bytes MAC. Expecting 16 bytes for key block version B. MAC: '9AA5BBA6'"), 614 | (16, "C0024M3TC00E0100TT04BBA6", "Key block MAC is malformed. Received 4 bytes MAC. Expecting 8 bytes for key block version C. MAC: 'BBA6'"), 615 | (16, "D0032P0AE00E0000205FDF791C3548EC", "Key block MAC is malformed. Received 16 bytes MAC. Expecting 32 bytes for key block version D. MAC: '205FDF791C3548EC'"), 616 | 617 | (16, "A0088M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF3544689103X9AA5BBA6", "Encrypted key must be valid hexchars. Key data: '62C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF3544689103X'"), 618 | (16, "B0088M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF35X468910379AA5BBA6", "Encrypted key must be valid hexchars. Key data: '62C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF35X'"), 619 | (16, "C0088M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF3544689103X9AA5BBA6", "Encrypted key must be valid hexchars. Key data: '62C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF3544689103X'"), 620 | (16, "D0112P0AE00E0000DDF7B73888F22B757600010215895621B94A4E8DA57DD3E01BB66FF046A4E6BX9B8F5C30BDD3A946205FDF791C3548EC", "Encrypted key must be valid hexchars. Key data: 'DDF7B73888F22B757600010215895621B94A4E8DA57DD3E01BB66FF046A4E6BX'"), 621 | 622 | (16, "A0024M3TC00E00009AA5BBA6", "Encrypted key is malformed. Key data: ''"), 623 | (16, "B0032M3TC00E0000FFFFFFFF9AA5BBA6", "Encrypted key is malformed. Key data: ''"), 624 | (16, "C0024M3TC00E00009AA5BBA6", "Encrypted key is malformed. Key data: ''"), 625 | (16, "D0048P0AE00E00009B8F5C30BDD3A946205FDF791C3548EC", "Encrypted key is malformed. Key data: ''"), 626 | 627 | (16, "A0056M3TC00E0000BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB9AA5BBA6", "Key block MAC doesn't match generated MAC."), 628 | (16, "B0064M3TC00E0000BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFFFFFFFF9AA5BBA6", "Key block MAC doesn't match generated MAC."), 629 | (16, "C0056M3TC00E0000BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB9AA5BBA6", "Key block MAC doesn't match generated MAC."), 630 | (16, "D0112P0AE00E0000DDF7B73888F22B757600010215895621B94A4E8DA57DD3E01BB66FF046A4E6B89B8F5C30BDD3A946205FDF791C3548E4", "Key block MAC doesn't match generated MAC."), 631 | 632 | (7, "A0088M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF354468910379AA5BBA6", "KBPK length (7) must be Single, Double or Triple DES for key block version A."), 633 | (8, "B0088M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF354468910379AA5BBA6", "KBPK length (8) must be Double or Triple DES for key block version B."), 634 | (7, "C0088M3TC00E000062C2C14D8785A01A9E8283525CA96F490D0CC6346FC7C2AC1E6FF354468910379AA5BBA6", "KBPK length (7) must be Single, Double or Triple DES for key block version C."), 635 | (19, "D0112P0AE00E0000DDF7B73888F22B757600010215895621B94A4E8DA57DD3E01BB66FF046A4E6B89B8F5C30BDD3A946205FDF791C3548E4", "KBPK length (19) must be AES-128, AES-192 or AES-256 for key block version D."), 636 | 637 | # These keys have length set to 3 bits And key lengths that do not add up to a byte are not supported. 638 | # KBPK must be b"E"*16. 639 | (16, "A0056M3TC00E0000C6F4C83842160CBA48D98A1218862857124FAF46", "Decrypted key is invalid."), 640 | (16, "B0064M3TC00E0000F74E0A3502C5CEE07342D5DE9E72135E4A81944F80691F0F", "Decrypted key is invalid."), 641 | (16, "C0056M3TC00E0000F71573EB7441BB50A5C4511893AFB37B5B95A4AD", "Decrypted key is invalid."), 642 | (16, "D0080M3TC00E000007E81A7F29A870D4A0CD5AB27E9FEC4A8863E879B11EA3A0ADA406AD26D35B2F", "Decrypted key is invalid."), 643 | 644 | # DES key length is set to 128 bits while the key is 64 bits. KBPK must be b"E"*16. 645 | # AES key length is set to 256 bits while the key is 128 bits. KBPK must be b"E"*16. 646 | (16, "A0056M3TC00E0000EF14FD71CFCDCE0630AD5C1CDE0041DCF95CF1D0", "Decrypted key is malformed."), 647 | (16, "B0064M3TC00E00000398DC96A5DDB0EF61E26F8935173BD478DF9484050A672A", "Decrypted key is malformed."), 648 | (16, "C0056M3TC00E000001235EC22408B6CE866746FF992B8707FD7A26D2", "Decrypted key is malformed."), 649 | (16, "D0112P0AE00E00000DC02E4C2B63120403CC732FB1B17E6D44138E7C341AE7368DEAD6FB4673F25ECFD803F1101F701A7FE8BD3516D3D1BF", "Decrypted key is malformed."), 650 | ], 651 | ) 652 | # fmt: on 653 | def test_kb_unwrap_exceptions(kbpk_len: int, kb: str, error: str) -> None: 654 | """Test unwrap exceptions""" 655 | with pytest.raises(tr31.KeyBlockError) as e: 656 | _ = tr31.KeyBlock(b"E" * kbpk_len).unwrap(kb) 657 | assert e.value.args[0] == error 658 | 659 | 660 | def test_kb_unwrap_exceptions_unsupported_version() -> None: 661 | """Test unwrap exceptions""" 662 | kb = tr31.KeyBlock(b"E" * 16) 663 | # Have to cheat an explictly remove support for version B 664 | save_b = kb._unwrap_dispatch["B"] 665 | del kb._unwrap_dispatch["B"] 666 | with pytest.raises(tr31.KeyBlockError) as e: 667 | _ = kb.unwrap( 668 | "B0064M3TC00E00000398DC96A5DDB0EF61E26F8935173BD478DF9484050A672A" 669 | ) 670 | kb._unwrap_dispatch["B"] = save_b 671 | assert e.value.args[0] == "Key block version ID (B) is not supported." 672 | 673 | 674 | def test_wrap_unwrap_functions() -> None: 675 | kbpk = b"\xAB" * 16 676 | key = b"\xCD" * 16 677 | kb = tr31.wrap(kbpk, "B0096P0TE00N0000", key) 678 | h_out, key_out = tr31.unwrap(kbpk, kb) 679 | assert key == key_out 680 | assert h_out.version_id == "B" 681 | assert h_out.key_usage == "P0" 682 | assert h_out.algorithm == "T" 683 | assert h_out.mode_of_use == "E" 684 | assert h_out.version_num == "00" 685 | assert h_out.exportability == "N" 686 | assert h_out.reserved == "00" 687 | assert len(h_out.blocks) == 0 688 | -------------------------------------------------------------------------------- /psec/tr31.py: -------------------------------------------------------------------------------- 1 | r"""TR-31 2018 2 | 3 | Wrap/Unwrap 4 | ~~~~~~~~~~~ 5 | 6 | This module provides two convenient functions to wrap and unwrap TR-31 key block. 7 | 8 | To wrap a key into TR-31 key block:: 9 | 10 | >>> import psec 11 | >>> psec.tr31.wrap( 12 | ... kbpk=b"FFFFFFFFEEEEEEEE", 13 | ... header="B0000P0TE00N0000", 14 | ... key=b"CCCCCCCCDDDDDDDD") # doctest: +SKIP 15 | 'B0096P0TE00N0000A800A7D1A4C0C1BE762177E1CC59D84844EB67C9F6432B2CA34187AE2E0385EBEE2231697BC5DAE8' 16 | 17 | To unwrap a key from TR-31 key block:: 18 | 19 | >>> import psec 20 | >>> header, key = psec.tr31.unwrap( 21 | ... kbpk=b"FFFFFFFFEEEEEEEE", 22 | ... key_block="B0096P0TE00N0000A800A7D1A4C0C1BE762177E1CC59D84844EB67C9F6432B2CA34187AE2E0385EBEE2231697BC5DAE8") 23 | >>> key 24 | b'CCCCCCCCDDDDDDDD' 25 | 26 | Alternativelly, KeyBlock class can be used to achieve the same thing:: 27 | 28 | >>> import psec 29 | >>> kb = psec.tr31.KeyBlock(kbpk=b"FFFFFFFFEEEEEEEE", header="B0000P0TE00N0000") 30 | >>> kb.wrap(key=b"CCCCCCCCDDDDDDDD") # doctest: +SKIP 31 | 'B0096P0TE00N00001BB4881919DA54DEB9BB1D340783BBD431A03A9BC577662C80F2EF9F95940EBD35C29B94C4BC0CCF' 32 | >>> kb.unwrap(key_block="B0096P0TE00N00001BB4881919DA54DEB9BB1D340783BBD431A03A9BC577662C80F2EF9F95940EBD35C29B94C4BC0CCF") 33 | b'CCCCCCCCDDDDDDDD' 34 | 35 | Header 36 | ~~~~~~ 37 | 38 | TR-31 header is parsed and verified when KeyBlock class is instanciated 39 | with an input header and when KeyBlock unwraps a raw TR-31 key block. 40 | 41 | >>> import psec 42 | >>> kb = psec.tr31.KeyBlock(kbpk=b"FFFFFFFFEEEEEEEE", header="B0000P0TE00N0100KS1800604B120F9292800000") 43 | >>> kb.header.version_id 44 | 'B' 45 | >>> kb.header.key_usage 46 | 'P0' 47 | >>> kb.header.algorithm 48 | 'T' 49 | >>> kb.header.mode_of_use 50 | 'E' 51 | >>> kb.header.version_num 52 | '00' 53 | >>> kb.header.exportability 54 | 'N' 55 | >>> kb.header.blocks["KS"] 56 | '00604B120F9292800000' 57 | 58 | And when this KeyBlock wraps a key it will use this exact header. 59 | 60 | >>> import psec 61 | >>> kb = psec.tr31.KeyBlock(kbpk=b"FFFFFFFFEEEEEEEE", header="B0000P0TE00N0100KS1800604B120F9292800000") 62 | >>> kb.wrap(key=b'CCCCCCCCDDDDDDDD') # doctest: +SKIP 63 | 'B0120P0TE00N0100KS1800604B120F92928000001D97E1A8BE3F9ABA19B1BBC382D22CED362727320CEC3EFABF4DC3CDEC8B5026771FFE9FEB2C525B' 64 | 65 | Header Properties 66 | ~~~~~~~~~~~~~~~~~ 67 | 68 | This library does NOT enforce or verify key block key usage, algorithm, 69 | mode of use and so on except for the format requirements. 70 | 71 | This library DOES make sure that provided key block header belongs to the key 72 | block and is not substituted. And upon successful validation/unwrapping it 73 | provides parsed key block header properties. 74 | 75 | These properties, such as key usage, algorithm, mode of use and so on, can then 76 | be ignored or enforced as a separate step by the library user. 77 | 78 | The following sections go into details on what key usage, algorithm and so on 79 | are formally allowed by the specification. 80 | 81 | Header Properties - Version ID 82 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 83 | +-------+-------------------------------------------------------------------+-----------+ 84 | | Value | Description | Algorithm | 85 | +=======+===================================================================+===========+ 86 | | 'A' | Key block protected using the Key Variant Binding Method. | DES | 87 | | | This version is deprecated. | | 88 | +-------+-------------------------------------------------------------------+-----------+ 89 | | 'B' | Key block protected using the TDES Key Derivation Binding Method. | DES | 90 | | | Recommended over versions 'A' and 'C' for new implementations. | | 91 | +-------+-------------------------------------------------------------------+-----------+ 92 | | 'C' | Key block protected using the TDES Key Variant Binding Method. | DES | 93 | | | Same as 'A' with some header value clarifications. | | 94 | +-------+-------------------------------------------------------------------+-----------+ 95 | | 'D' | Key block protected using the AES Key Derivation Binding Method. | AES | 96 | +-------+-------------------------------------------------------------------+-----------+ 97 | 98 | Numeric version IDs are reserved for proprietary key block definitions. 99 | This library supports only version IDs A, B, C and D. 100 | 101 | Header Properties - Key Usage 102 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 103 | Key usage defines the type of key. 104 | For example, a PIN encryption key or EMV Secure Messaging for Integrity key. 105 | 106 | +-------+------------------------------------------------------+----------------+ 107 | | Value | Description | Mode of Use | 108 | +=======+======================================================+================+ 109 | | 'B0' | BDK Base Derivation Key | 'X' | 110 | +-------+------------------------------------------------------+----------------+ 111 | | 'B1' | Initial DUKPT Key | 'X' | 112 | +-------+------------------------------------------------------+----------------+ 113 | | 'B2' | Base Key Variant Key | 'X' | 114 | +-------+------------------------------------------------------+----------------+ 115 | | 'C0' | CVK Card Verification Key | 'C', 'G', 'V' | 116 | +-------+------------------------------------------------------+----------------+ 117 | | 'D0' | Symmetric Key for Data Encryption | 'B', 'D', 'E' | 118 | +-------+------------------------------------------------------+----------------+ 119 | | 'D1' | Asymmetric Key for Data Encryption | 'B', 'D', 'E' | 120 | +-------+------------------------------------------------------+----------------+ 121 | | 'D2' | Data Encryption Key for Decimalization Table | 'B', 'D', 'E' | 122 | +-------+------------------------------------------------------+----------------+ 123 | | 'E0' | EMV/chip Issuer Master Key: Application cryptograms | 'X' | 124 | +-------+------------------------------------------------------+----------------+ 125 | | 'E1' | EMV/chip Issuer Master Key: Secure Messaging for | 'X' | 126 | | | Confidentiality | | 127 | +-------+------------------------------------------------------+----------------+ 128 | | 'E2' | EMV/chip Issuer Master Key: Secure Messaging for | 'X' | 129 | | | Integrity | | 130 | +-------+------------------------------------------------------+----------------+ 131 | | 'E3' | EMV/chip Issuer Master Key: Data Authentication Code | 'X' | 132 | +-------+------------------------------------------------------+----------------+ 133 | | 'E4' | EMV/chip Issuer Master Key: Dynamic Numbers | 'X' | 134 | +-------+------------------------------------------------------+----------------+ 135 | | 'E5' | EMV/chip Issuer Master Key: Card Personalization | 'X' | 136 | +-------+------------------------------------------------------+----------------+ 137 | | 'E6' | EMV/chip Issuer Master Key: Other | 'X' | 138 | +-------+------------------------------------------------------+----------------+ 139 | | 'I0' | Initialization Vector (IV) | 'N' | 140 | +-------+------------------------------------------------------+----------------+ 141 | | 'K0' | Key Encryption or wrapping | 'B', 'D', 'E' | 142 | +-------+------------------------------------------------------+----------------+ 143 | | 'K1' | TR-31 Key Block Protection Key | 'B', 'D', 'E' | 144 | +-------+------------------------------------------------------+----------------+ 145 | | 'K2' | TR-34 Asymmetric key | 'B', 'D', 'E' | 146 | +-------+------------------------------------------------------+----------------+ 147 | | 'K3' | Asymmetric key for key agreement/key wrapping | 'B', 'D', 'E', | 148 | | | | 'X' | 149 | +-------+------------------------------------------------------+----------------+ 150 | | 'M0' | ISO 16609 MAC algorithm 1 (using TDEA) | 'C', 'G', 'V' | 151 | +-------+------------------------------------------------------+----------------+ 152 | | 'M1' | ISO 9797-1 MAC Algorithm 1 | 'C', 'G', 'V' | 153 | +-------+------------------------------------------------------+----------------+ 154 | | 'M2' | ISO 9797-1 MAC Algorithm 2 | 'C', 'G', 'V' | 155 | +-------+------------------------------------------------------+----------------+ 156 | | 'M3' | ISO 9797-1 MAC Algorithm 3 | 'C', 'G', 'V' | 157 | +-------+------------------------------------------------------+----------------+ 158 | | 'M4' | ISO 9797-1 MAC Algorithm 4 | 'C', 'G', 'V' | 159 | +-------+------------------------------------------------------+----------------+ 160 | | 'M5' | ISO 9797-1:1999 MAC Algorithm 5 | 'C', 'G', 'V' | 161 | +-------+------------------------------------------------------+----------------+ 162 | | 'M6' | ISO 9797-1:2011 MAC Algorithm 5/CMAC | 'C', 'G', 'V' | 163 | +-------+------------------------------------------------------+----------------+ 164 | | 'M7' | HMAC | 'C', 'G', 'V' | 165 | +-------+------------------------------------------------------+----------------+ 166 | | 'M8' | ISO 9797-1:2011 MAC Algorithm 6 | 'C', 'G', 'V' | 167 | +-------+------------------------------------------------------+----------------+ 168 | | 'P0' | PIN Encryption | 'B', 'D', 'E' | 169 | +-------+------------------------------------------------------+----------------+ 170 | | 'S0' | Asymmetric key pair for digital signature | 'S', 'V' | 171 | +-------+------------------------------------------------------+----------------+ 172 | | 'S1' | Asymmetric key pair, CA key | 'S', 'V' | 173 | +-------+------------------------------------------------------+----------------+ 174 | | 'S2' | Asymmetric key pair, non-X9.24 key | 'S', 'V', 'T', | 175 | | | | 'B', 'D', 'E' | 176 | +-------+------------------------------------------------------+----------------+ 177 | | 'V0' | PIN verification, KPV, other algorithm | 'C', 'G', 'V' | 178 | +-------+------------------------------------------------------+----------------+ 179 | | 'V1' | PIN verification, IBM 3624 | 'C', 'G', 'V' | 180 | +-------+------------------------------------------------------+----------------+ 181 | | 'V2' | PIN Verification, VISA PVV | 'C', 'G', 'V' | 182 | +-------+------------------------------------------------------+----------------+ 183 | | 'V3' | PIN Verification, X9.132 algorithm 1 | 'C', 'G', 'V' | 184 | +-------+------------------------------------------------------+----------------+ 185 | | 'V4' | PIN Verification, X9.132 algorithm 2 | 'C', 'G', 'V' | 186 | +-------+------------------------------------------------------+----------------+ 187 | 188 | Numeric key usage values are reserved for proprietary use. 189 | 190 | Header Properties - Algorithm 191 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 192 | Algorithm defines what algorithms can be used with the key. 193 | 194 | +-------+----------------------------------------------------------------+ 195 | | Value | Definition | 196 | +=======+================================================================+ 197 | | 'A' | AES | 198 | +-------+----------------------------------------------------------------+ 199 | | 'D' | DES (included for backwards compatibility) | 200 | +-------+----------------------------------------------------------------+ 201 | | 'E' | Elliptic Curve | 202 | +-------+----------------------------------------------------------------+ 203 | | 'H' | HMAC (specify the underlying hash algorithm in optional block) | 204 | +-------+----------------------------------------------------------------+ 205 | | 'R' | RSA | 206 | +-------+----------------------------------------------------------------+ 207 | | 'S' | DSA | 208 | +-------+----------------------------------------------------------------+ 209 | | 'T' | Triple DES (TDES) | 210 | +-------+----------------------------------------------------------------+ 211 | 212 | Numeric algorithm values are reserved for proprietary use. 213 | 214 | Header Properties - Mode of Use 215 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 216 | Mode of use defines what operation the key can perform. 217 | 218 | +-------+----------------------------------------+ 219 | | Value | Definition | 220 | +=======+========================================+ 221 | | 'B' | Both Encrypt & Decrypt / Wrap & Unwrap | 222 | +-------+----------------------------------------+ 223 | | 'C' | Both Generate & Verify | 224 | +-------+----------------------------------------+ 225 | | 'E' | Encrypt / Wrap Only | 226 | +-------+----------------------------------------+ 227 | | 'G' | Generate Only | 228 | +-------+----------------------------------------+ 229 | | 'N' | No special restrictions other than | 230 | | | restrictions implied by the Key Usage | 231 | +-------+----------------------------------------+ 232 | | 'S' | Signature Only | 233 | +-------+----------------------------------------+ 234 | | 'T' | Both Sign & Decrypt | 235 | +-------+----------------------------------------+ 236 | | 'V' | Verify Only | 237 | +-------+----------------------------------------+ 238 | | 'X' | Key used to derive other key(s) | 239 | +-------+----------------------------------------+ 240 | | 'Y' | Key used to create key variants | 241 | +-------+----------------------------------------+ 242 | 243 | Numeric mode of use values are reserved for proprietary use. 244 | 245 | Header Properties - Key Version Number 246 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 247 | Not to be confused with version ID. 248 | 249 | Key version number can be used to: 250 | 251 | * Prevent re-injection of old keys 252 | * Specify that value carried in the key block is a component of a key. 253 | 254 | +-----------+-----------+--------------------------------------------------------------+ 255 | | First | Second | Meaning | 256 | | Character | Character | | 257 | +===========+===========+==============================================================+ 258 | | '0' | '0' | Key versioning is not used for this key. | 259 | +-----------+-----------+--------------------------------------------------------------+ 260 | | 'c' | Any | The value carried in this key block is a component of a key. | 261 | | | | Local rules will dictate the proper use of a component | 262 | +-----------+-----------+--------------------------------------------------------------+ 263 | | Any other combination | The key version field indicates the version of the key | 264 | | | carried in the key block. The value may optionally be used | 265 | | | to prevent re-injection of old keys. | 266 | +-----------------------+--------------------------------------------------------------+ 267 | 268 | Header Properties - Exportability 269 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 270 | Exportability refers to transfer outside the cryptographic domain 271 | in which the key is found. 272 | 273 | +-------+-------------------------------------------+ 274 | | Value | Definition | 275 | +=======+===========================================+ 276 | | 'E' | Exportable under trusted key | 277 | +-------+-------------------------------------------+ 278 | | 'N' | Not exportable | 279 | +-------+-------------------------------------------+ 280 | | 'S' | Sensitive, exportable under untrusted key | 281 | +-------+-------------------------------------------+ 282 | 283 | Numeric exportability values are reserved for proprietary use. 284 | 285 | Header Properties - Conclusion 286 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 287 | 288 | Let's create a key block for: 289 | 290 | * a TDES PIN encryption key 291 | * that can be used for encryption only 292 | * that does not have any versioning 293 | * that is not exportable 294 | 295 | >>> import psec 296 | >>> header = psec.tr31.Header( 297 | ... version_id="B", # Version B as recommended for TDES 298 | ... key_usage="P0", # PIN Encryption 299 | ... algorithm="T", # TDES 300 | ... mode_of_use="E", # Encryption only 301 | ... version_num="00", # No version 302 | ... exportability="N" # Not exportable 303 | ... ) 304 | >>> kb = psec.tr31.KeyBlock(kbpk=b"FFFFFFFFEEEEEEEE", header=header) 305 | >>> kb.wrap(key=b"CCCCCCCCDDDDDDDD") # doctest: +SKIP 306 | 'B0096P0TE00N0000C805321ACF492D2256C3220BE81D542AC8B1DAC9AA30BD7B94E4185F8473A005CB8BD9C7812EAD16' 307 | >>> str(kb.header) 308 | 'B0016P0TE00N0000' 309 | 310 | Let's create another key block for: 311 | 312 | * an AES MAC key (ISO 9797-1 MAC Algorithm 3) 313 | * that can be used for MAC generation and verification 314 | * that does not have any versioning 315 | * that is not exportable 316 | 317 | >>> import psec 318 | >>> header = psec.tr31.Header( 319 | ... version_id="D", # Version D is for AES 320 | ... key_usage="M3", # PIN Encryption 321 | ... algorithm="A", # AES 322 | ... mode_of_use="C", # Both Generate & Verify 323 | ... version_num="00", # No version 324 | ... exportability="N" # Not exportable 325 | ... ) 326 | >>> kb = psec.tr31.KeyBlock(kbpk=b"FFFFFFFFEEEEEEEE", header=header) 327 | >>> kb.wrap(key=b"CCCCCCCCDDDDDDDD") # doctest: +SKIP 328 | 'D0144M3AC00N00008C1CB96C82BFFB2DFA31216647B06D3EC65375F9EC676ADD47B949551E5EC3A82873D6C65C963789119B4A90E7446EA04A4861D4688759EAB83C8D59FEF3DAD1' 329 | >>> str(kb.header) 330 | 'D0016M3AC00N0000' 331 | 332 | Optional Blocks 333 | ~~~~~~~~~~~~~~~ 334 | 335 | This library support encoding and decoding of optional blocks. 336 | However, it does not verify or enforce block IDs and their contents. 337 | 338 | To add an optional block to the header, add a new key to blocks dictionary 339 | inside header: 340 | 341 | >>> import psec 342 | >>> header = psec.tr31.Header( 343 | ... version_id="B", # Version B as recommended for TDES 344 | ... key_usage="P0", # PIN Encryption 345 | ... algorithm="T", # TDES 346 | ... mode_of_use="E", # Encryption only 347 | ... version_num="00", # No version 348 | ... exportability="N" # Not exportable 349 | ... ) 350 | >>> header.blocks["TT"] = "HelloWorld" 351 | >>> kb = psec.tr31.KeyBlock(kbpk=b"FFFFFFFFEEEEEEEE", header=header) 352 | >>> kb.wrap(key=b"CCCCCCCCDDDDDDDD") # doctest: +SKIP 353 | 'B0120P0TE00N0200TT0EHelloWorldPB0A0000008FE0CA0F00BAA9A1E3695A6C051FAB695FCE07E289BC0151D096909BB87670F63238670402E90A37' 354 | >>> kb.header.blocks["TT"] 355 | 'HelloWorld' 356 | >>> str(kb.header) 357 | 'B0040P0TE00N0200TT0EHelloWorldPB0A000000' 358 | 359 | Similarly, to access optional block, access blocks dictionary inside header: 360 | 361 | >>> import psec 362 | >>> kb = psec.tr31.KeyBlock(kbpk=b"FFFFFFFFEEEEEEEE") 363 | >>> kb.unwrap(key_block="B0120P0TE00N0200TT0EHelloWorldPB0A0000008FE0CA0F00BAA9A1E3695A6C051FAB695FCE07E289BC0151D096909BB87670F63238670402E90A37") 364 | b'CCCCCCCCDDDDDDDD' 365 | >>> kb.header.blocks["TT"] 366 | 'HelloWorld' 367 | >>> str(kb.header) 368 | 'B0040P0TE00N0200TT0EHelloWorldPB0A000000' 369 | """ 370 | 371 | import typing as _typing 372 | from os import urandom as _urandom 373 | 374 | from psec import aes as _aes 375 | from psec import des as _des 376 | from psec import mac as _mac 377 | from psec import tools as _tools 378 | 379 | __all__ = [ 380 | "KeyBlock", 381 | "KeyBlockError", 382 | "Header", 383 | "HeaderError", 384 | "wrap", 385 | "unwrap", 386 | ] 387 | 388 | 389 | class HeaderError(ValueError): 390 | """Subclass of ValueError that indicates error in processing TR-31 header data.""" 391 | 392 | pass 393 | 394 | 395 | class KeyBlockError(ValueError): 396 | """Subclass of ValueError that indicates error in processing TR-31 key block data.""" 397 | 398 | pass 399 | 400 | 401 | class Blocks(_typing.MutableMapping[str, str]): 402 | def __init__(self) -> None: 403 | self._blocks: _typing.Dict[str, str] = {} 404 | 405 | def __len__(self) -> int: 406 | return len(self._blocks) 407 | 408 | def __getitem__(self, key: str) -> str: 409 | if key in self._blocks: 410 | return self._blocks[key] 411 | raise KeyError(key) 412 | 413 | def __setitem__(self, key: str, item: str) -> None: 414 | if len(key) != 2 or not _tools.ascii_alphanumeric(key): 415 | raise HeaderError( 416 | f"Block ID ({key}) is invalid. Expecting 2 alphanumeric characters." 417 | ) 418 | if not _tools.ascii_printable(item): 419 | raise HeaderError( 420 | f"Block {key} data is invalid. Expecting ASCII printable characters. " 421 | f"Data: '{item}'" 422 | ) 423 | self._blocks[key] = item 424 | 425 | def __delitem__(self, key: str) -> None: 426 | del self._blocks[key] 427 | 428 | def __iter__(self) -> _typing.Iterator[str]: 429 | return iter(self._blocks) 430 | 431 | def __contains__(self, key: object) -> bool: 432 | return key in self._blocks 433 | 434 | def __repr__(self) -> str: 435 | return repr(self._blocks) 436 | 437 | def dump(self, algo_block_size: int) -> _typing.Tuple[int, str]: 438 | """Format TR-31 header optional blocks into a string. 439 | 440 | Parameters 441 | ---------- 442 | algo_block_size : int 443 | TR-31 algorithm block size. 8 for TDES and 16 for AES. 444 | Required because produced block data must be multiple of 445 | algorithm encryption block size. 446 | 447 | Returns 448 | ------- 449 | blocks_num : int 450 | Number of blocks included in the produced string. 451 | blocks : str 452 | String that contains TR-31 header optional blocks. 453 | 454 | Raises 455 | ------ 456 | HeaderError 457 | """ 458 | 459 | blocks_list: _typing.List[str] = [] 460 | for block_id, block_data in self.items(): 461 | blocks_list.append(block_id) 462 | 463 | # Length is encoded in a single hexchar pair for <=255 fields. 464 | # +4 is to include block ID and length itself into the length. 465 | if len(block_data) + 4 <= 255: 466 | blocks_list.append( 467 | (len(block_data) + 4).to_bytes(1, "big").hex().upper() 468 | ) 469 | else: 470 | # For fields longer than 255 construct an extended length 471 | # that consits of length of length and only then actual length. 472 | # +10 is to include block ID, extended length indicator (00), 473 | # length of length (02) and length itself (e.g. 0FFF). 474 | blocks_list.append("0002") 475 | try: 476 | blocks_list.append( 477 | (len(block_data) + 10).to_bytes(2, "big").hex().upper() 478 | ) 479 | except OverflowError: 480 | raise HeaderError( 481 | f"Block {block_id} length ({str(len(block_data))}) is too long." 482 | ) from None 483 | 484 | blocks_list.append(block_data) 485 | 486 | blocks = "".join(blocks_list) 487 | 488 | # If total block data is not multiple of encryption algo block size 489 | # then need to add a Pad Block. 490 | if len(blocks) % algo_block_size != 0: 491 | pad_num = algo_block_size - ((len(blocks) + 4) % algo_block_size) 492 | pb_block = ( 493 | "PB" + (4 + pad_num).to_bytes(1, "big").hex().upper() + (pad_num * "0") 494 | ) 495 | pb_block_count = 1 496 | else: 497 | pb_block = "" 498 | pb_block_count = 0 499 | 500 | if len(self) + pb_block_count > 99: 501 | raise HeaderError( 502 | f"Number of blocks ({str(len(self) + pb_block_count)}) " 503 | f"exceeds limit of 99." 504 | ) 505 | 506 | return len(self) + pb_block_count, blocks + pb_block 507 | 508 | def load(self, blocks_num: int, blocks: str) -> int: 509 | """Load TR-31 header optional blocks from a string. 510 | 511 | Parameters 512 | ---------- 513 | blocks_num : int 514 | Number of expected optional blocks within the supplied string. 515 | blocks : str 516 | String that contains TR-31 header optional blocks. 517 | 518 | Returns 519 | ------- 520 | blocks_len : int 521 | Length of parsed optional blocks data within supplied input string. 522 | 523 | Raises 524 | ------ 525 | HeaderError 526 | 527 | Notes 528 | ----- 529 | This method clears all current optional blocks before loading new ones. 530 | """ 531 | 532 | def parse_extended_len( 533 | block_id: str, blocks: str, i: int 534 | ) -> _typing.Tuple[int, int]: 535 | # Get 2 character long optional block length of length. 536 | # E.g. if a block's length is 0190 then this field is set to 02 537 | # to indicate that the length consists of 2 hexchar pairs. 538 | block_len_len_s = blocks[i : i + 2] 539 | if len(block_len_len_s) != 2 or not _tools.ascii_hexchar(block_len_len_s): 540 | raise HeaderError( 541 | f"Block {block_id} length of length ({block_len_len_s}) is malformed. " 542 | f"Expecting 2 hexchars." 543 | ) 544 | i += 2 545 | 546 | block_len_len = int(block_len_len_s, 16) * 2 547 | if block_len_len == 0: 548 | raise HeaderError(f"Block {block_id} length of length must not be 0.") 549 | 550 | # Extract actual block length 551 | block_len_s = blocks[i : i + block_len_len] 552 | if len(block_len_s) != block_len_len or not _tools.ascii_hexchar( 553 | block_len_s 554 | ): 555 | raise HeaderError( 556 | f"Block {block_id} length ({block_len_s}) is malformed. " 557 | f"Expecting {str(block_len_len)} hexchars." 558 | ) 559 | 560 | # Block length includes ID, 00 length indicator, 561 | # lenght of length and actual length in it. 562 | # Remove that to return block data length. 563 | block_len = int(block_len_s, 16) 564 | return (block_len - 6 - block_len_len), i + block_len_len 565 | 566 | # Remove any existing blocks before loading new ones 567 | self.clear() 568 | 569 | i = 0 570 | for _ in range(0, blocks_num): 571 | # Get 2 character long optional block ID 572 | block_id = blocks[i : i + 2] 573 | if len(block_id) != 2: 574 | raise HeaderError(f"Block ID ({block_id}) is malformed.") 575 | i += 2 576 | 577 | # Get 2 character long optional block length. 578 | block_len_s = blocks[i : i + 2] 579 | if len(block_len_s) != 2 or not _tools.ascii_hexchar(block_len_s): 580 | raise HeaderError( 581 | f"Block {block_id} length ({block_len_s}) is malformed. " 582 | f"Expecting 2 hexchars." 583 | ) 584 | i += 2 585 | 586 | # If the length is 00 then block length consists of multiple bytes. 587 | # Otherwise, the first length byte is the length. 588 | block_len = int(block_len_s, 16) 589 | if block_len == 0: 590 | block_len, i = parse_extended_len(block_id, blocks, i) 591 | else: 592 | # Exclude block ID and block length to get block data length 593 | block_len -= 4 594 | 595 | if block_len < 0: 596 | raise HeaderError( 597 | f"Block {block_id} length does not include block ID and length." 598 | ) 599 | 600 | block_data = blocks[i : i + block_len] 601 | if len(block_data) != block_len: 602 | raise HeaderError( 603 | f"Block {block_id} data is malformed. " 604 | f"Received {str(len(block_data))}/{str(block_len)}. " 605 | f"Block data: '{block_data}'" 606 | ) 607 | i += block_len 608 | 609 | # Do not add Pad Block. It's there to make optional blocks 610 | # multiple of encryption block size. It does not cary any data. 611 | if block_id.upper() != "PB": 612 | self[block_id] = block_data 613 | 614 | return i 615 | 616 | 617 | class Header: 618 | """TR-31 header. Supports versions A, B, C and D. 619 | 620 | Parameters 621 | ---------- 622 | version_id : str 623 | Identifies the version of the key block, which defines the method 624 | by which it is cryptographically protected as well as the content and 625 | layout of the key block: 626 | 627 | - A - TDES variant. Deprecated and should not be used for new applications. 628 | - B - TDES key derivation. Preferred TDES implementation. 629 | - C - TDES variant. Same as A. 630 | - D - AES key derivation 631 | 632 | key_usage : str 633 | Provides information about the intended function of the protected 634 | key/sensitive data. For example, caculating MAC. 635 | algorithm: str 636 | The approved algorithm for which the protected key may be used. 637 | mode_of_use: str 638 | Defines the operation the protected key can perform. For example, 639 | a MAC key may be limited to verification only. 640 | version_num: str 641 | Two-digit ASCII character version number, optionally used to 642 | indicate that contents of the key block is a component, 643 | or to prevent re-injection of old keys. 644 | Not to be confused with version ID. 645 | exportability: str 646 | Defines whether the protected key may be transferred outside 647 | the cryptographic domain in which the key is found. 648 | 649 | Attributes 650 | ---------- 651 | version_id : str 652 | Identifies the version of the key block, which defines the method 653 | by which it is cryptographically protected as well as the content and 654 | layout of the key block: 655 | 656 | - A = TDES variant. Deprecated and should not be used for new applications. 657 | - B = TDES key derivation. Preferred TDES implementation. 658 | - C = TDES variant. Same as A. 659 | - D = AES key derivation 660 | 661 | key_usage : str 662 | Provides information about the intended function of the protected 663 | key/sensitive data. For example, caculating MAC. 664 | algorithm: str 665 | The approved algorithm for which the protected key may be used. 666 | mode_of_use: str 667 | Defines the operation the protected key can perform. For example, 668 | a MAC key may be limited to verification only. 669 | version_num: str 670 | Two-digit ASCII character version number, optionally used to 671 | indicate that contents of the key block is a component, 672 | or to prevent re-injection of old keys. 673 | Not to be confused with version ID. 674 | exportability: str 675 | Defines whether the protected key may be transferred outside 676 | the cryptographic domain in which the key is found. 677 | blocks : Blocks 678 | A dictionary of optional blocks that contain additional 679 | information about the key block. 680 | """ 681 | 682 | _version_id_key_block_mac_len = {"A": 4, "B": 8, "C": 4, "D": 16} 683 | 684 | # DES = 8 bytes block 685 | # AES = 16 bytes block 686 | _version_id_algo_block_size = {"A": 8, "B": 8, "C": 8, "D": 16} 687 | 688 | def __init__( 689 | self, 690 | version_id: str = "B", 691 | key_usage: str = "00", 692 | algorithm: str = "0", 693 | mode_of_use: str = "0", 694 | version_num: str = "00", 695 | exportability: str = "N", 696 | ) -> None: 697 | self.version_id = version_id 698 | self.key_usage = key_usage 699 | self.algorithm = algorithm 700 | self.mode_of_use = mode_of_use 701 | self.version_num = version_num 702 | self.exportability = exportability 703 | self._reserved = "00" 704 | self.blocks = Blocks() 705 | 706 | def __str__(self) -> str: 707 | blocks_num, blocks = self.blocks.dump( 708 | self._version_id_algo_block_size[self.version_id] 709 | ) 710 | return ( 711 | self.version_id 712 | + str(16 + len(blocks)).zfill(4) 713 | + self.key_usage 714 | + self.algorithm 715 | + self.mode_of_use 716 | + self.version_num 717 | + self.exportability 718 | + str(blocks_num).zfill(2) 719 | + self.reserved 720 | + blocks 721 | ) 722 | 723 | @property 724 | def version_id(self) -> str: 725 | """Identifies the version of the key block, which defines the method 726 | by which it is cryptographically protected as well as the content and 727 | layout of the block. 728 | """ 729 | return self._version_id 730 | 731 | @version_id.setter 732 | def version_id(self, version_id: str) -> None: 733 | if version_id not in {"A", "B", "C", "D"}: 734 | raise HeaderError(f"Version ID ({version_id}) is not supported.") 735 | self._version_id = version_id 736 | 737 | @property 738 | def key_usage(self) -> str: 739 | """Provides information about the intended function of the protected 740 | key/sensitive data. For example, caculating MAC. 741 | """ 742 | return self._key_usage 743 | 744 | @key_usage.setter 745 | def key_usage(self, key_usage: str) -> None: 746 | if len(key_usage) != 2 or not _tools.ascii_alphanumeric(key_usage): 747 | raise HeaderError(f"Key usage ({key_usage}) is invalid.") 748 | self._key_usage = key_usage 749 | 750 | @property 751 | def algorithm(self) -> str: 752 | """The approved algorithm for which the protected key may be used.""" 753 | return self._algorithm 754 | 755 | @algorithm.setter 756 | def algorithm(self, algorithm: str) -> None: 757 | if len(algorithm) != 1 or not _tools.ascii_alphanumeric(algorithm): 758 | raise HeaderError(f"Algorithm ({algorithm}) is invalid.") 759 | self._algorithm = algorithm 760 | 761 | @property 762 | def mode_of_use(self) -> str: 763 | """Defines the operation the protected key can perform. For example, 764 | a MAC key may be limited to verification only. 765 | """ 766 | return self._mode_of_use 767 | 768 | @mode_of_use.setter 769 | def mode_of_use(self, mode_of_use: str) -> None: 770 | if len(mode_of_use) != 1 or not _tools.ascii_alphanumeric(mode_of_use): 771 | raise HeaderError(f"Mode of use ({mode_of_use}) is invalid.") 772 | self._mode_of_use = mode_of_use 773 | 774 | @property 775 | def version_num(self) -> str: 776 | """Two-digit ASCII character version number, optionally used to 777 | indicate that contents of the key block is a component, 778 | or to prevent re-injection of old keys. 779 | Not to be confused with version ID. 780 | """ 781 | return self._version_num 782 | 783 | @version_num.setter 784 | def version_num(self, version_num: str) -> None: 785 | if len(version_num) != 2 or not _tools.ascii_alphanumeric(version_num): 786 | raise HeaderError(f"Version number ({version_num}) is invalid.") 787 | self._version_num = version_num 788 | 789 | @property 790 | def exportability(self) -> str: 791 | """Defines whether the protected key may be transferred outside 792 | the cryptographic domain in which the key is found. 793 | """ 794 | return self._exportability 795 | 796 | @exportability.setter 797 | def exportability(self, exportability: str) -> None: 798 | if len(exportability) != 1 or not _tools.ascii_alphanumeric(exportability): 799 | raise HeaderError(f"Exportability ({exportability}) is invalid.") 800 | self._exportability = exportability 801 | 802 | @property 803 | def reserved(self) -> str: 804 | """This field is reserved for future use. 805 | It should be filled with zeroes. 806 | """ 807 | return self._reserved 808 | 809 | def dump(self, key_len: int) -> str: 810 | """Format TR-31 header into a key block string 811 | 812 | Parameters 813 | ---------- 814 | key_len : int 815 | Length of key to be wrapped into this key block. 816 | Key length is required to determine correct key block length. 817 | 818 | Returns 819 | ------- 820 | header : str 821 | String that contains TR-31 header. 822 | 823 | Raises 824 | ------ 825 | HeaderError 826 | """ 827 | 828 | algo_block_size = self._version_id_algo_block_size[self.version_id] 829 | pad_len = algo_block_size - ((2 + key_len) % algo_block_size) 830 | 831 | blocks_num, blocks = self.blocks.dump(algo_block_size) 832 | 833 | kb_len = ( 834 | 16 # mandatory header 835 | + 4 # key length's length in ASCII 836 | + (key_len * 2) 837 | + (pad_len * 2) 838 | + (self._version_id_key_block_mac_len[self.version_id] * 2) 839 | + len(blocks) 840 | ) 841 | 842 | if kb_len > 9999: 843 | raise HeaderError( 844 | f"Total key block length ({str(kb_len)}) exceeds limit of 9999." 845 | ) 846 | 847 | return ( 848 | self.version_id 849 | + str(kb_len).zfill(4) 850 | + self.key_usage 851 | + self.algorithm 852 | + self.mode_of_use 853 | + self.version_num 854 | + self.exportability 855 | + str(blocks_num).zfill(2) 856 | + self.reserved 857 | + blocks 858 | ) 859 | 860 | def load(self, header: str) -> int: 861 | """Load TR-31 header from a string 862 | 863 | Parameters 864 | ---------- 865 | header : str 866 | String that contains TR-31 header information. 867 | Could also be a complete or incomplete TR-31 key block. 868 | 869 | Returns 870 | ------- 871 | header_len : int 872 | Length of parsed header data within supplied input string. 873 | 874 | Raises 875 | ------ 876 | HeaderError 877 | 878 | Notes 879 | ----- 880 | This method overrides all values of the header and 881 | clears all prior optional blocks before loading new ones. 882 | """ 883 | 884 | if not _tools.ascii_alphanumeric(header[:16]): 885 | raise HeaderError( 886 | f"Header must be ASCII alphanumeric. Header: '{header[:16]}'" 887 | ) 888 | 889 | if len(header) < 16: 890 | raise HeaderError( 891 | f"Header length ({str(len(header))}) must be >=16. " 892 | f"Header: '{header[:16]}'" 893 | ) 894 | 895 | self.version_id = header[0] 896 | self.key_usage = header[5:7] 897 | self.algorithm = header[7] 898 | self.mode_of_use = header[8] 899 | self.version_num = header[9:11] 900 | self.exportability = header[11] 901 | self._reserved = header[14:16] 902 | 903 | if not _tools.ascii_numeric(header[12:14]): 904 | raise HeaderError( 905 | f"Number of blocks ({header[12:14]}) is invalid. " 906 | f"Expecting 2 digits." 907 | ) 908 | 909 | blocks_num = int(header[12:14]) 910 | blocks_len = self.blocks.load(blocks_num, header[16:]) 911 | 912 | return 16 + blocks_len 913 | 914 | 915 | class KeyBlock: 916 | """TR-31 key block. Supports versions A, B, C and D. 917 | 918 | Parameters 919 | ---------- 920 | kbpk : bytes 921 | Key Block Protection Key. 922 | Must be a Single, Double or Triple DES key for versions A and C. 923 | Must be a Double or Triple DES key for versions B. 924 | Must be an AES key for version D. 925 | header : Header or str 926 | TR-31 key block header either in TR-31 string format or 927 | as a Header class. Optional. 928 | A full TR-31 key block in string format can be provided 929 | to extract header from. 930 | 931 | Attributes 932 | ---------- 933 | kbpk : bytes 934 | Key Block Protection Key. 935 | Must be a Single, Double or Triple DES key for versions A and C. 936 | Must be a Double or Triple DES key for versions B. 937 | Must be an AES key for version D. 938 | header : Header 939 | TR-31 key block header. 940 | 941 | Notes 942 | ----- 943 | It's highly recommended that the length of the KBPK is equal or greater 944 | than the length of the key to be protected. E.g. do not protect AES-256 key 945 | with AES-128 KBPK. 946 | """ 947 | 948 | _version_id_key_block_mac_len = {"A": 4, "B": 8, "C": 4, "D": 16} 949 | 950 | # DES = 8 bytes block 951 | # AES = 16 bytes block 952 | _version_id_algo_block_size = {"A": 8, "B": 8, "C": 8, "D": 16} 953 | 954 | # DES = 24 bytes (Triple DES) 955 | # AES = 32 byets (AES-256) 956 | _algo_id_max_key_len = {"T": 24, "D": 24, "A": 32} 957 | 958 | def __init__( 959 | self, kbpk: bytes, header: _typing.Optional[_typing.Union[Header, str]] = None 960 | ) -> None: 961 | self.kbpk = kbpk 962 | 963 | if isinstance(header, str): 964 | self.header = Header() 965 | self.header.load(header) 966 | elif isinstance(header, Header): 967 | self.header = header 968 | else: 969 | self.header = Header() 970 | 971 | def __str__(self) -> str: 972 | return str(self.header) 973 | 974 | def wrap(self, key: bytes, masked_key_len: _typing.Optional[int] = None) -> str: 975 | r"""Wrap key into a TR-31 key block version A, B, C or D. 976 | 977 | Parameters 978 | ---------- 979 | key : bytes 980 | A key to be wrapped. 981 | masked_key_len : int, optional 982 | Desired key length in bytes to mask true key length. 983 | Defaults to max key size for algorithm: 984 | 985 | - Triple DES for DES algorithm (24 bytes) 986 | - AES-256 for AES algorithm (32 bytes) 987 | 988 | Returns 989 | ------- 990 | key_block : str 991 | Key formatted in a TR-31 key block and encrypted 992 | under the KBPK. 993 | 994 | Raises 995 | ------ 996 | KeyBlockError 997 | HeaderError 998 | 999 | Notes 1000 | ----- 1001 | TR-31 version C is identical to version A with exception 1002 | of some of the key headers values that have been clarified. 1003 | 1004 | Examples 1005 | -------- 1006 | >>> import psec 1007 | >>> h = psec.tr31.Header("B", "P0", "T","E","00","N") 1008 | >>> kb = psec.tr31.KeyBlock(kbpk=b"\xFF" * 16, header=h) 1009 | >>> kb.wrap(key=b"\xEE" * 16) # doctest: +SKIP 1010 | 'B0096P0TE00N0000342811F905093F2B797EB9248C1121C011C2AE41BEC63E33C9C2FDB320540D82327221AE9C5C34FB' 1011 | >>> kb.header.version_id 1012 | 'B' 1013 | >>> kb.header.key_usage 1014 | 'P0' 1015 | >>> kb.header.algorithm 1016 | 'T' 1017 | >>> kb.header.mode_of_use 1018 | 'E' 1019 | >>> kb.header.version_num 1020 | '00' 1021 | >>> kb.header.exportability 1022 | 'N' 1023 | """ 1024 | 1025 | try: 1026 | wrap = self._wrap_dispatch[self.header.version_id] 1027 | except KeyError: 1028 | raise KeyBlockError( 1029 | f"Key block version ID ({self.header.version_id}) is not supported." 1030 | ) from None 1031 | 1032 | if masked_key_len is None: 1033 | masked_key_len = max( 1034 | self._algo_id_max_key_len.get(self.header.algorithm, len(key)), len(key) 1035 | ) 1036 | else: 1037 | masked_key_len = max(masked_key_len, len(key)) 1038 | 1039 | return wrap( 1040 | self, 1041 | self.header.dump(masked_key_len), 1042 | key, 1043 | masked_key_len - len(key), 1044 | ) 1045 | 1046 | def unwrap(self, key_block: str) -> bytes: 1047 | r"""Unwrap key from a TR-31 key block version A, B, C or D. 1048 | 1049 | Parameters 1050 | ---------- 1051 | key_block : str 1052 | A TR-31 key block. 1053 | 1054 | Returns 1055 | ------- 1056 | key : bytes 1057 | Unwrapped key. The unwrapped key is guaranteed to be what the sender 1058 | wrapped into the block. However, it does not guarantee that the sender 1059 | wrapped a valid key. 1060 | 1061 | Raises 1062 | ------ 1063 | KeyBlockError 1064 | HeaderError 1065 | 1066 | Notes 1067 | ----- 1068 | TR-31 version C is identical to version A with exception 1069 | of some of the key headers values that have been clarified. 1070 | 1071 | Examples 1072 | -------- 1073 | >>> import psec 1074 | >>> kb = psec.tr31.KeyBlock(kbpk=b"\xFF" * 16) 1075 | >>> kb.unwrap("B0096P0TE00N0000342811F905093F2B797EB9248C1121C011C2AE41BEC63E33C9C2FDB320540D82327221AE9C5C34FB") 1076 | b'\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee' 1077 | >>> kb.header.version_id 1078 | 'B' 1079 | >>> kb.header.key_usage 1080 | 'P0' 1081 | >>> kb.header.algorithm 1082 | 'T' 1083 | >>> kb.header.mode_of_use 1084 | 'E' 1085 | >>> kb.header.version_num 1086 | '00' 1087 | >>> kb.header.exportability 1088 | 'N' 1089 | """ 1090 | 1091 | # Extract header from the key block 1092 | header_len = self.header.load(key_block) 1093 | 1094 | # Verify block length 1095 | if not _tools.ascii_numeric(key_block[1:5]): 1096 | raise KeyBlockError( 1097 | f"Key block header length ({key_block[1:5]}) is malformed. " 1098 | f"Expecting 4 digits." 1099 | ) 1100 | 1101 | key_block_len = int(key_block[1:5]) 1102 | if key_block_len != len(key_block): 1103 | raise KeyBlockError( 1104 | f"Key block header length ({str(key_block_len)}) " 1105 | f"doesn't match input data length ({str(len(key_block))})." 1106 | ) 1107 | 1108 | if ( 1109 | len(key_block) % self._version_id_algo_block_size[self.header.version_id] 1110 | != 0 1111 | ): 1112 | raise KeyBlockError( 1113 | f"Key block length ({str(len(key_block))}) must be multiple of " 1114 | f"{str(self._version_id_algo_block_size[self.header.version_id])} " 1115 | f"for key block version {self.header.version_id}." 1116 | ) 1117 | 1118 | # Extract MAC from the key block. 1119 | # MAC length is fixed for each version ID. 1120 | algo_mac_len = self._version_id_key_block_mac_len[self.header.version_id] 1121 | received_mac_s = key_block[header_len:][-algo_mac_len * 2 :] 1122 | try: 1123 | received_mac = bytes.fromhex(received_mac_s) 1124 | except ValueError: 1125 | raise KeyBlockError( 1126 | f"Key block MAC must be valid hexchars. MAC: '{received_mac_s}'" 1127 | ) from None 1128 | 1129 | if len(received_mac) != algo_mac_len: 1130 | raise KeyBlockError( 1131 | f"Key block MAC is malformed. Received {str(len(received_mac_s))} bytes MAC. " 1132 | f"Expecting {str(algo_mac_len * 2)} bytes for key block version {self.header.version_id}. " 1133 | f"MAC: '{received_mac_s}'" 1134 | ) 1135 | 1136 | # Extract encrypted key data from the key block. 1137 | # Whatever is left after taking header and MAC out is the key data. 1138 | key_data_s = key_block[header_len:][: -algo_mac_len * 2] 1139 | try: 1140 | key_data = bytes.fromhex(key_data_s) 1141 | except ValueError: 1142 | raise KeyBlockError( 1143 | f"Encrypted key must be valid hexchars. Key data: '{key_data_s}'" 1144 | ) from None 1145 | 1146 | try: 1147 | unwrap = self._unwrap_dispatch[self.header.version_id] 1148 | except KeyError: 1149 | raise KeyBlockError( 1150 | f"Key block version ID ({self.header.version_id}) is not supported." 1151 | ) from None 1152 | 1153 | return unwrap(self, key_block[:header_len], key_data, received_mac) 1154 | 1155 | # Version B 1156 | 1157 | def _b_wrap(self, header: str, key: bytes, extra_pad: int) -> str: 1158 | """Wrap key into TR-31 key block version B""" 1159 | 1160 | if len(self.kbpk) not in {16, 24}: 1161 | raise KeyBlockError( 1162 | f"KBPK length ({str(len(self.kbpk))}) must be Double or Triple DES " 1163 | f"for key block version {self.header.version_id}." 1164 | ) 1165 | 1166 | # Derive Key Block Encryption and Authentication Keys 1167 | kbek, kbak = self._b_derive() 1168 | 1169 | # Format key data: 2 byte key length measured in bits + key + pad 1170 | pad_len = 8 - ((2 + len(key) + extra_pad) % 8) 1171 | pad = _urandom(pad_len + extra_pad) 1172 | clear_key_data = (len(key) * 8).to_bytes(2, "big") + key + pad 1173 | 1174 | # Generate MAC 1175 | mac = self._b_generate_mac(kbak, header, clear_key_data) 1176 | 1177 | # Encrypt key data 1178 | enc_key = _des.encrypt_tdes_cbc(kbek, mac, clear_key_data) 1179 | 1180 | return header + enc_key.hex().upper() + mac.hex().upper() 1181 | 1182 | def _b_unwrap(self, header: str, key_data: bytes, received_mac: bytes) -> bytes: 1183 | """Unwrap key from TR-31 key block version B""" 1184 | 1185 | if len(self.kbpk) not in {16, 24}: 1186 | raise KeyBlockError( 1187 | f"KBPK length ({str(len(self.kbpk))}) must be Double or Triple DES " 1188 | f"for key block version {self.header.version_id}." 1189 | ) 1190 | 1191 | if len(key_data) < 8 or len(key_data) % 8 != 0: 1192 | raise KeyBlockError( 1193 | f"Encrypted key is malformed. Key data: '{key_data.hex().upper()}'" 1194 | ) 1195 | 1196 | # Derive Key Block Encryption and Authentication Keys 1197 | kbek, kbak = self._b_derive() 1198 | 1199 | # Decrypt key data 1200 | clear_key_data = _des.decrypt_tdes_cbc(kbek, received_mac, key_data) 1201 | 1202 | # Validate MAC 1203 | mac = self._b_generate_mac(kbak, header, clear_key_data) 1204 | if mac != received_mac: 1205 | raise KeyBlockError("Key block MAC doesn't match generated MAC.") 1206 | 1207 | # Extract key from key data: 2 byte key length measured in bits + key + pad 1208 | key_length = int.from_bytes(clear_key_data[0:2], "big") 1209 | 1210 | # This library does not support keys not measured in whole bytes 1211 | if key_length % 8 != 0: 1212 | raise KeyBlockError("Decrypted key is invalid.") 1213 | 1214 | key_length = key_length // 8 1215 | key = clear_key_data[2 : key_length + 2] 1216 | if len(key) != key_length: 1217 | raise KeyBlockError("Decrypted key is malformed.") 1218 | 1219 | return key 1220 | 1221 | def _b_derive(self) -> _typing.Tuple[bytes, bytes]: 1222 | """Derive Key Block Encryption and Authentication Keys""" 1223 | # Key Derivation data 1224 | # byte 0 = a counter increment for each block of kbpk, start at 1 1225 | # byte 1-2 = key usage indicator 1226 | # - 0000 = encryption 1227 | # - 0001 = MAC 1228 | # byte 3 = separator, set to 0 1229 | # byte 4-5 = algorithm indicator 1230 | # - 0000 = 2-Key TDES 1231 | # - 0001 = 3-Key TDES 1232 | # byte 6-7 = key length in bits 1233 | # - 0080 = 2-Key TDES 1234 | # - 00C0 = 3-Key TDES 1235 | kd_input = bytearray(b"\x01\x00\x00\x00\x00\x00\x00\x80") 1236 | 1237 | if len(self.kbpk) == 16: 1238 | # Adjust for 2-key TDES 1239 | kd_input[4:6] = b"\x00\x00" 1240 | kd_input[6:8] = b"\x00\x80" 1241 | calls_to_cmac = [1, 2] 1242 | else: 1243 | # Adjust for 3-key TDES 1244 | kd_input[4:6] = b"\x00\x01" 1245 | kd_input[6:8] = b"\x00\xC0" 1246 | calls_to_cmac = [1, 2, 3] 1247 | 1248 | kbek = bytearray() # encryption key 1249 | kbak = bytearray() # authentication key 1250 | 1251 | k1, _ = self._derive_des_cmac_subkey(self.kbpk) 1252 | 1253 | # Produce the same number of keying material as the key's length. 1254 | # Each call to CMAC produces 64 bits of keying material. 1255 | # 2-key DES -> 2 calls to CMAC -> 2-key DES KBEK/KBAK 1256 | # 3-key DES -> 3 calls to CMAC -> 3-key DES KBEK/KBAK 1257 | for i in calls_to_cmac: 1258 | # Counter is incremented for each call to CMAC 1259 | kd_input[0] = i 1260 | 1261 | # Encryption key 1262 | kd_input[1:3] = b"\x00\x00" 1263 | kbek += _mac.generate_cbc_mac( 1264 | self.kbpk, _tools.xor(kd_input, k1), 1, 8, _mac.Algorithm.DES 1265 | ) 1266 | 1267 | # Authentication key 1268 | kd_input[1:3] = b"\x00\x01" 1269 | kbak += _mac.generate_cbc_mac( 1270 | self.kbpk, _tools.xor(kd_input, k1), 1, 8, _mac.Algorithm.DES 1271 | ) 1272 | 1273 | return bytes(kbek), bytes(kbak) 1274 | 1275 | def _b_generate_mac(self, kbak: bytes, header: str, key_data: bytes) -> bytes: 1276 | """Generate MAC using KBAK""" 1277 | km1, _ = self._derive_des_cmac_subkey(kbak) 1278 | mac_data = header.encode("ascii") + key_data 1279 | mac_data = mac_data[:-8] + _tools.xor(mac_data[-8:], km1) 1280 | mac = _mac.generate_cbc_mac(kbak, mac_data, 1, 8, _mac.Algorithm.DES) 1281 | return mac 1282 | 1283 | def _derive_des_cmac_subkey(self, key: bytes) -> _typing.Tuple[bytes, bytes]: 1284 | """Derive two subkeys from a DES key. Each subkey is 8 bytes.""" 1285 | 1286 | def shift_left_1(in_bytes: bytes) -> bytes: 1287 | """Shift byte array left by 1 bit""" 1288 | in_bytes = bytearray(in_bytes) 1289 | in_bytes[0] = in_bytes[0] & 0b01111111 1290 | int_in = int.from_bytes(in_bytes, "big") << 1 1291 | return int.to_bytes(int_in, len(in_bytes), "big") 1292 | 1293 | r64 = b"\x00\x00\x00\x00\x00\x00\x00\x1B" 1294 | 1295 | s = _des.encrypt_tdes_ecb(key, b"\x00" * 8) 1296 | 1297 | if s[0] & 0b10000000: 1298 | k1 = _tools.xor(shift_left_1(s), r64) 1299 | else: 1300 | k1 = shift_left_1(s) 1301 | 1302 | if k1[0] & 0b10000000: 1303 | k2 = _tools.xor(shift_left_1(k1), r64) 1304 | else: 1305 | k2 = shift_left_1(k1) 1306 | 1307 | return k1, k2 1308 | 1309 | # Version A, C. 1310 | 1311 | def _c_wrap(self, header: str, key: bytes, extra_pad: int) -> str: 1312 | """Wrap key into TR-31 key block version A or C""" 1313 | 1314 | if len(self.kbpk) not in {8, 16, 24}: 1315 | raise KeyBlockError( 1316 | f"KBPK length ({str(len(self.kbpk))}) must be Single, Double or Triple DES " 1317 | f"for key block version {self.header.version_id}." 1318 | ) 1319 | 1320 | # Derive Key Block Encryption and Authentication Keys 1321 | kbek, kbak = self._c_derive() 1322 | 1323 | # Format key data: 2 byte key length measured in bits + key + pad 1324 | pad_len = 8 - ((2 + len(key) + extra_pad) % 8) 1325 | pad = _urandom(pad_len + extra_pad) 1326 | clear_key_data = (len(key) * 8).to_bytes(2, "big") + key + pad 1327 | 1328 | # Encrypt key data 1329 | enc_key = _des.encrypt_tdes_cbc( 1330 | kbek, header.encode("ascii")[:8], clear_key_data 1331 | ) 1332 | 1333 | # Generate MAC 1334 | mac = self._c_generate_mac(kbak, header, enc_key) 1335 | 1336 | return header + enc_key.hex().upper() + mac.hex().upper() 1337 | 1338 | def _c_unwrap(self, header: str, key_data: bytes, received_mac: bytes) -> bytes: 1339 | """Unwrap key from TR-31 key block version A or C""" 1340 | 1341 | if len(self.kbpk) not in {8, 16, 24}: 1342 | raise KeyBlockError( 1343 | f"KBPK length ({str(len(self.kbpk))}) must be Single, Double or Triple DES " 1344 | f"for key block version {self.header.version_id}." 1345 | ) 1346 | 1347 | if len(key_data) < 8 or len(key_data) % 8 != 0: 1348 | raise KeyBlockError( 1349 | f"Encrypted key is malformed. Key data: '{key_data.hex().upper()}'" 1350 | ) 1351 | 1352 | # Derive Key Block Encryption and Authentication Keys 1353 | kbek, kbak = self._c_derive() 1354 | 1355 | # Validate MAC 1356 | mac = self._c_generate_mac(kbak, header, key_data) 1357 | if mac != received_mac: 1358 | raise KeyBlockError("Key block MAC doesn't match generated MAC.") 1359 | 1360 | # Decrypt key data 1361 | clear_key_data = _des.decrypt_tdes_cbc( 1362 | kbek, header.encode("ascii")[:8], key_data 1363 | ) 1364 | 1365 | # Extract key from key data: 2 byte key length measured in bits + key + pad 1366 | key_length = int.from_bytes(clear_key_data[0:2], "big") 1367 | 1368 | # This library does not support keys not measured in whole bytes 1369 | if key_length % 8 != 0: 1370 | raise KeyBlockError("Decrypted key is invalid.") 1371 | 1372 | key_length = key_length // 8 1373 | key = clear_key_data[2 : key_length + 2] 1374 | if len(key) != key_length: 1375 | raise KeyBlockError("Decrypted key is malformed.") 1376 | 1377 | return key 1378 | 1379 | def _c_derive(self) -> _typing.Tuple[bytes, bytes]: 1380 | """Derive Key Block Encryption and Authentication Keys""" 1381 | return ( 1382 | _tools.xor(self.kbpk, b"\x45" * len(self.kbpk)), # Encryption Key 1383 | _tools.xor(self.kbpk, b"\x4D" * len(self.kbpk)), # Authentication Key 1384 | ) 1385 | 1386 | def _c_generate_mac(self, kbak: bytes, header: str, key_data: bytes) -> bytes: 1387 | """Generate MAC using KBAK""" 1388 | return _mac.generate_cbc_mac( 1389 | kbak, header.encode("ascii") + key_data, 1, 4, _mac.Algorithm.DES 1390 | ) 1391 | 1392 | # Versiond D 1393 | 1394 | def _d_wrap(self, header: str, key: bytes, extra_pad: int) -> str: 1395 | """Wrap key into TR-31 key block version D""" 1396 | 1397 | if len(self.kbpk) not in {16, 24, 32}: 1398 | raise KeyBlockError( 1399 | f"KBPK length ({str(len(self.kbpk))}) must be AES-128, AES-192 or AES-256 " 1400 | f"for key block version {self.header.version_id}." 1401 | ) 1402 | 1403 | # Derive Key Block Encryption and Authentication Keys 1404 | kbek, kbak = self._d_derive() 1405 | 1406 | # Format key data: 2 byte key length measured in bits + key + pad 1407 | pad_len = 16 - ((2 + len(key) + extra_pad) % 16) 1408 | pad = _urandom(pad_len + extra_pad) 1409 | clear_key_data = (len(key) * 8).to_bytes(2, "big") + key + pad 1410 | 1411 | # Generate MAC 1412 | mac = self._d_generate_mac(kbak, header, clear_key_data) 1413 | 1414 | # Encrypt key data 1415 | enc_key = _aes.encrypt_aes_cbc(kbek, mac, clear_key_data) 1416 | 1417 | return header + enc_key.hex().upper() + mac.hex().upper() 1418 | 1419 | def _d_unwrap(self, header: str, key_data: bytes, received_mac: bytes) -> bytes: 1420 | """Unwrap key from TR-31 key block version D""" 1421 | 1422 | if len(self.kbpk) not in {16, 24, 32}: 1423 | raise KeyBlockError( 1424 | f"KBPK length ({str(len(self.kbpk))}) must be AES-128, AES-192 or AES-256 " 1425 | f"for key block version {self.header.version_id}." 1426 | ) 1427 | 1428 | if len(key_data) < 16 or len(key_data) % 16 != 0: 1429 | raise KeyBlockError( 1430 | f"Encrypted key is malformed. Key data: '{key_data.hex().upper()}'" 1431 | ) 1432 | 1433 | # Derive Key Block Encryption and Authentication Keys 1434 | kbek, kbak = self._d_derive() 1435 | 1436 | # Decrypt key data 1437 | clear_key_data = _aes.decrypt_aes_cbc(kbek, received_mac, key_data) 1438 | 1439 | # Validate MAC 1440 | mac = self._d_generate_mac(kbak, header, clear_key_data) 1441 | if mac != received_mac: 1442 | raise KeyBlockError("Key block MAC doesn't match generated MAC.") 1443 | 1444 | # Extract key from key data: 2 byte key length measured in bits + key + pad 1445 | key_length = int.from_bytes(clear_key_data[0:2], "big") 1446 | 1447 | # This library does not support keys not measured in whole bytes 1448 | if key_length % 8 != 0: 1449 | raise KeyBlockError("Decrypted key is invalid.") 1450 | 1451 | key_length = key_length // 8 1452 | key = clear_key_data[2 : key_length + 2] 1453 | if len(key) != key_length: 1454 | raise KeyBlockError("Decrypted key is malformed.") 1455 | 1456 | return key 1457 | 1458 | def _d_derive(self) -> _typing.Tuple[bytes, bytes]: 1459 | """Derive Key Block Encryption and Authentication Keys""" 1460 | # Key Derivation data 1461 | # byte 0 = a counter increment for each block of kbpk, start at 1 1462 | # byte 1-2 = key usage indicator 1463 | # - 0000 = encryption 1464 | # - 0001 = MAC 1465 | # byte 3 = separator, set to 0 1466 | # byte 4-5 = algorithm indicator 1467 | # - 0002 = AES-128 1468 | # - 0003 = AES-192 1469 | # - 0004 = AES-256 1470 | # byte 6-7 = key length in bits 1471 | # - 0080 = AES-128 1472 | # - 00C0 = AES-192 1473 | # - 0100 = AES-256 1474 | kd_input = bytearray( 1475 | b"\x01\x00\x00\x00\x00\x02\x00\x80\x80\x00\x00\x00\x00\x00\x00\x00" 1476 | ) 1477 | 1478 | if len(self.kbpk) == 16: 1479 | # Adjust for AES 128 bit 1480 | kd_input[4:6] = b"\x00\x02" 1481 | kd_input[6:8] = b"\x00\x80" 1482 | calls_to_cmac = [1] 1483 | elif len(self.kbpk) == 24: 1484 | # Adjust for AES 192 bit 1485 | kd_input[4:6] = b"\x00\x03" 1486 | kd_input[6:8] = b"\x00\xC0" 1487 | calls_to_cmac = [1, 2] 1488 | else: 1489 | # Adjust for AES 256 bit 1490 | kd_input[4:6] = b"\x00\x04" 1491 | kd_input[6:8] = b"\x01\x00" 1492 | calls_to_cmac = [1, 2] 1493 | 1494 | kbek = bytearray() # encryption key 1495 | kbak = bytearray() # authentication key 1496 | 1497 | _, k2 = self._derive_aes_cmac_subkey(self.kbpk) 1498 | 1499 | # Produce the same number of keying material as the key's length. 1500 | # Each call to CMAC produces 128 bits of keying material. 1501 | # AES-128 -> 1 call to CMAC -> AES-128 KBEK/KBAK 1502 | # AES-196 -> 2 calls to CMAC -> AES-196 KBEK/KBAK (out of 256 bits of data) 1503 | # AES-256 -> 2 calls to CMAC -> AES-256 KBEK/KBAK 1504 | for i in calls_to_cmac: 1505 | # Counter is incremented for each call to CMAC 1506 | kd_input[0] = i 1507 | 1508 | # Encryption key 1509 | kd_input[1:3] = b"\x00\x00" 1510 | kbek += _mac.generate_cbc_mac( 1511 | self.kbpk, _tools.xor(kd_input, k2), 1, 16, _mac.Algorithm.AES 1512 | ) 1513 | 1514 | # Authentication key 1515 | kd_input[1:3] = b"\x00\x01" 1516 | kbak += _mac.generate_cbc_mac( 1517 | self.kbpk, _tools.xor(kd_input, k2), 1, 16, _mac.Algorithm.AES 1518 | ) 1519 | 1520 | return bytes(kbek[: len(self.kbpk)]), bytes(kbak[: len(self.kbpk)]) 1521 | 1522 | def _d_generate_mac(self, kbak: bytes, header: str, key_data: bytes) -> bytes: 1523 | """Generate MAC using KBAK""" 1524 | km1, _ = self._derive_aes_cmac_subkey(kbak) 1525 | mac_data = header.encode("ascii") + key_data 1526 | mac_data = mac_data[:-16] + _tools.xor(mac_data[-16:], km1) 1527 | mac = _mac.generate_cbc_mac(kbak, mac_data, 1, 16, _mac.Algorithm.AES) 1528 | return mac 1529 | 1530 | def _derive_aes_cmac_subkey(self, key: bytes) -> _typing.Tuple[bytes, bytes]: 1531 | """Derive two subkeys from an AES key. Each subkey is 16 bytes.""" 1532 | 1533 | def shift_left_1(in_bytes: bytes) -> bytes: 1534 | """Shift byte array left by 1 bit""" 1535 | in_bytes = bytearray(in_bytes) 1536 | in_bytes[0] = in_bytes[0] & 0b01111111 1537 | int_in = int.from_bytes(in_bytes, "big") << 1 1538 | return int.to_bytes(int_in, len(in_bytes), "big") 1539 | 1540 | r64 = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x87" 1541 | 1542 | s = _aes.encrypt_aes_ecb(key, b"\x00" * 16) 1543 | 1544 | if s[0] & 0b10000000: 1545 | k1 = _tools.xor(shift_left_1(s), r64) 1546 | else: 1547 | k1 = shift_left_1(s) 1548 | 1549 | if k1[0] & 0b10000000: 1550 | k2 = _tools.xor(shift_left_1(k1), r64) 1551 | else: 1552 | k2 = shift_left_1(k1) 1553 | 1554 | return k1, k2 1555 | 1556 | _wrap_dispatch: _typing.Dict[ 1557 | str, _typing.Callable[["KeyBlock", str, bytes, int], str] 1558 | ] = { 1559 | "A": _c_wrap, 1560 | "B": _b_wrap, 1561 | "C": _c_wrap, 1562 | "D": _d_wrap, 1563 | } 1564 | 1565 | _unwrap_dispatch: _typing.Dict[ 1566 | str, _typing.Callable[["KeyBlock", str, bytes, bytes], bytes] 1567 | ] = { 1568 | "A": _c_unwrap, 1569 | "B": _b_unwrap, 1570 | "C": _c_unwrap, 1571 | "D": _d_unwrap, 1572 | } 1573 | 1574 | 1575 | def wrap( 1576 | kbpk: bytes, 1577 | header: _typing.Union[Header, str], 1578 | key: bytes, 1579 | masked_key_len: _typing.Optional[int] = None, 1580 | ) -> str: 1581 | r"""Wrap key into a TR-31 key block version A, B, C or D. 1582 | 1583 | Parameters 1584 | ---------- 1585 | kbpk : bytes 1586 | Key Block Protection Key. 1587 | Must be a Single, Double or Triple DES key for versions A and C. 1588 | Must be a Double or Triple DES key for versions B. 1589 | Must be an AES key for version D. 1590 | header : Header or str 1591 | TR-31 key block header either in TR-31 string format or 1592 | as a Header class. 1593 | A full TR-31 key block in string format can be provided 1594 | to extract header from. 1595 | key : bytes 1596 | A key to be wrapped. 1597 | masked_key_len : int, optional 1598 | Desired key length in bytes to mask true key length. 1599 | Defaults to max key size for algorithm: 1600 | 1601 | - Triple DES for DES algorithm (24 bytes) 1602 | - AES-256 for AES algorithm (32 bytes) 1603 | 1604 | Returns 1605 | ------- 1606 | key_block : str 1607 | Key formatted in a TR-31 key block and encrypted 1608 | under the KBPK. 1609 | 1610 | Raises 1611 | ------ 1612 | KeyBlockError 1613 | HeaderError 1614 | 1615 | Notes 1616 | ----- 1617 | It's highly recommended that the length of the KBPK is equal or greater 1618 | than the length of the key to be protected. E.g. do not protect AES-256 key 1619 | with AES-128 KBPK. 1620 | 1621 | Examples 1622 | -------- 1623 | >>> import psec 1624 | >>> psec.tr31.wrap( 1625 | ... kbpk=b"\xAB" * 16, 1626 | ... header="B0096P0TE00N0000", 1627 | ... key=b"\xCD" * 16) # doctest: +SKIP 1628 | 'B0096P0TE00N0000471D4FBE35E5865BDE20DBF4C15503161F55D681170BF8DD14D01B6822EF8550CB67C569DE8AC048' 1629 | """ 1630 | return KeyBlock(kbpk, header).wrap(key, masked_key_len) 1631 | 1632 | 1633 | def unwrap(kbpk: bytes, key_block: str) -> _typing.Tuple[Header, bytes]: 1634 | r"""Unwrap key from a TR-31 key block version A, B, C or D. 1635 | 1636 | Parameters 1637 | ---------- 1638 | kbpk : bytes 1639 | Key Block Protection Key. 1640 | Must be a Single, Double or Triple DES key for versions A and C. 1641 | Must be a Double or Triple DES key for versions B. 1642 | Must be an AES key for version D. 1643 | key_block : str 1644 | A TR-31 key block. 1645 | 1646 | Returns 1647 | ------- 1648 | header : Header 1649 | TR-31 key block header. 1650 | key : bytes 1651 | Unwrapped key. The unwrapped key is guaranteed to be what the sender 1652 | wrapped into the block. However, it does not guarantee that the sender 1653 | wrapped a valid key. 1654 | 1655 | Raises 1656 | ------ 1657 | KeyBlockError 1658 | HeaderError 1659 | 1660 | Notes 1661 | ----- 1662 | It's highly recommended that the length of the KBPK is equal or greater 1663 | than the length of the key to be protected. E.g. do not protect AES-256 key 1664 | with AES-128 KBPK. 1665 | 1666 | Examples 1667 | -------- 1668 | >>> import psec 1669 | >>> header, key = psec.tr31.unwrap( 1670 | ... kbpk=b"\xAB" * 16, 1671 | ... key_block="B0096P0TE00N0000471D4FBE35E5865BDE20DBF4C15503161F55D681170BF8DD14D01B6822EF8550CB67C569DE8AC048") 1672 | >>> key 1673 | b'\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd' 1674 | >>> header.version_id 1675 | 'B' 1676 | >>> header.key_usage 1677 | 'P0' 1678 | >>> header.algorithm 1679 | 'T' 1680 | >>> header.mode_of_use 1681 | 'E' 1682 | >>> header.version_num 1683 | '00' 1684 | >>> header.exportability 1685 | 'N' 1686 | """ 1687 | kb = KeyBlock(kbpk) 1688 | return kb.header, kb.unwrap(key_block) 1689 | --------------------------------------------------------------------------------