├── asherah ├── py.typed ├── exceptions.py ├── __init__.py ├── scripts │ └── download-libasherah.sh ├── asherah.py └── types.py ├── tests ├── __init__.py └── test_asherah.py ├── CHANGELOG.md ├── .editorconfig ├── benchmark.py ├── SECURITY.md ├── .github └── workflows │ ├── test.yml │ ├── publish.yml │ └── codeql-analysis.yml ├── tox.ini ├── LICENSE ├── README.md ├── pyproject.toml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── poetry.lock /asherah/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.2.0] - 2022-03-08 2 | 3 | - Switch to using SetupJson instead of Setup for initialization 4 | 5 | ## [0.1.0] - 2022-03-04 6 | 7 | - Initial release 8 | -------------------------------------------------------------------------------- /asherah/exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom exceptions for Asherah""" 2 | 3 | 4 | class AsherahException(Exception): 5 | """Base exception class for any problems encountered in Asherah""" 6 | -------------------------------------------------------------------------------- /asherah/__init__.py: -------------------------------------------------------------------------------- 1 | """Asherah application encryption library""" 2 | 3 | from .asherah import Asherah 4 | from .types import AsherahConfig 5 | 6 | __all__ = ["Asherah", "AsherahConfig"] 7 | -------------------------------------------------------------------------------- /asherah/scripts/download-libasherah.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf asherah/libasherah/ 4 | 5 | wget --content-disposition --directory-prefix asherah/libasherah/ \ 6 | https://github.com/godaddy/asherah-cobhan/releases/download/v0.4.32/libasherah-arm64.dylib \ 7 | https://github.com/godaddy/asherah-cobhan/releases/download/v0.4.32/libasherah-arm64.so \ 8 | https://github.com/godaddy/asherah-cobhan/releases/download/v0.4.32/libasherah-x64.dylib \ 9 | https://github.com/godaddy/asherah-cobhan/releases/download/v0.4.32/libasherah-x64.so \ 10 | || exit 1 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | tab_width = 4 13 | max_line_length = 120 14 | 15 | # Use 2 spaces for the HTML files 16 | [*.html] 17 | indent_size = 2 18 | 19 | # The JSON files contain newlines inconsistently 20 | [*.json] 21 | indent_size = 2 22 | 23 | # Minified JavaScript files shouldn't be changed 24 | [**.min.js] 25 | 26 | # Makefiles always use tabs for indentation 27 | [Makefile] 28 | indent_style = tab 29 | 30 | # Batch files use tabs for indentation 31 | [*.bat] 32 | indent_style = tab 33 | 34 | [*.md] 35 | trim_trailing_whitespace = false 36 | 37 | [*.{yaml,yml}] 38 | indent_size = 2 39 | -------------------------------------------------------------------------------- /benchmark.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import timeit 3 | 4 | from asherah import Asherah, AsherahConfig 5 | 6 | config = AsherahConfig( 7 | kms="static", 8 | metastore="memory", 9 | service_name="TestService", 10 | product_id="TestProduct", 11 | enable_session_caching=True, 12 | ) 13 | crypt = Asherah() 14 | crypt.setup(config) 15 | 16 | data = b"mysecretdata" 17 | 18 | crypt_cycle = """ 19 | encrypted = crypt.encrypt("partition", data) 20 | decrypted = crypt.decrypt("partition", encrypted) 21 | """ 22 | 23 | print(f'Benchmarking encrypt/decrypt round trips of "{data}".') 24 | for loop_size in [100, 1000, 10000, 100000]: 25 | result = timeit.timeit( 26 | stmt=crypt_cycle, setup="gc.enable()", number=loop_size, globals=globals() 27 | ) 28 | print(f"Executed {loop_size} iterations in {result} seconds.") 29 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | We take security very seriously at GoDaddy. We appreciate your efforts to 4 | responsibly disclose your findings, and will make every effort to acknowledge 5 | your contributions. 6 | 7 | ## Where should I report security issues? 8 | 9 | In order to give the community time to respond and upgrade, we strongly urge you 10 | report all security issues privately. 11 | 12 | To report a security issue in one of our Open Source projects email us directly 13 | at **oss@godaddy.com** and include the word "SECURITY" in the subject line. 14 | 15 | This mail is delivered to our Open Source Security team. 16 | 17 | After the initial reply to your report, the team will keep you informed of the 18 | progress being made towards a fix and announcement, and may ask for additional 19 | information or guidance. 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | pull_request: 9 | branches: 10 | - main 11 | - develop 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Check out the repository 19 | uses: actions/checkout@v3 # Use the latest version of the checkout action 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v4 # Use the latest version of setup-python 23 | with: 24 | python-version: '3.10' 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install poetry 30 | poetry install 31 | 32 | - name: Download Asherah binaries 33 | run: | 34 | asherah/scripts/download-libasherah.sh 35 | 36 | - name: Run tests 37 | run: | 38 | poetry run pytest --cov # Run tests with coverage report 39 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.7.0 3 | toxworkdir = {env:TOX_WORK_DIR:.tox} 4 | skip_missing_interpreters = True 5 | envlist = py{37,38,39,310},black,mypy,pylint 6 | parallel_show_output = True 7 | isolated_build = True 8 | 9 | [gh-actions] 10 | python = 11 | 3.7: py37 12 | 3.8: py38 13 | 3.9: py39 14 | 3.10: py310 15 | 16 | [testenv] 17 | whitelist_externals = 18 | poetry 19 | pytest 20 | setenv = 21 | PYTHONDONTWRITEBYTECODE=1 22 | PYTHONHASHSEED=0 23 | PYTHONWARNINGS=ignore 24 | commands = 25 | poetry install --no-root -v 26 | poetry run pytest {posargs} 27 | 28 | [testenv:black] 29 | basepython = python3.7 30 | commands = 31 | poetry install --no-root -v 32 | poetry run black --check . 33 | 34 | [testenv:mypy] 35 | basepython = python3.7 36 | commands = 37 | poetry install --no-root -v 38 | poetry run mypy . 39 | 40 | [testenv:pylint] 41 | basepython = python3.7 42 | commands = 43 | poetry install --no-root -v 44 | poetry run pylint asherah/ tests/ 45 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] # Trigger when a release is published 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Check out the repository 13 | uses: actions/checkout@v3 # Use the latest version of the checkout action 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 # Use the latest version of setup-python 17 | with: 18 | python-version: '3.10' # Update to the latest stable version of Python 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install --upgrade poetry 24 | 25 | - name: Download Asherah binaries 26 | run: | 27 | asherah/scripts/download-libasherah.sh 28 | 29 | - name: Package and publish with Poetry 30 | run: | 31 | poetry config pypi-token.pypi $PYPI_TOKEN 32 | poetry publish --build 33 | env: 34 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 GoDaddy Operating Company, LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # asherah-python 2 | 3 | Asherah envelope encryption and key rotation library 4 | 5 | This is a wrapper of the Asherah Go implementation using the Cobhan FFI library 6 | 7 | ## Usage 8 | 9 | Example code: 10 | 11 | ```python 12 | from asherah import Asherah, AsherahConfig 13 | 14 | config = AsherahConfig( 15 | kms='static', 16 | metastore='memory', 17 | service_name='TestService', 18 | product_id='TestProduct', 19 | verbose=True, 20 | enable_session_caching=True 21 | ) 22 | crypt = Asherah() 23 | crypt.setup(config) 24 | 25 | data = b"mysecretdata" 26 | 27 | encrypted = crypt.encrypt("partition", data) 28 | print(encrypted) 29 | 30 | decrypted = crypt.decrypt("partition", encrypted) 31 | print(decrypted) 32 | ``` 33 | 34 | ## Benchmarking 35 | 36 | Included is a `benchmark.py` script that will give you an idea of the execution 37 | speeds you can see from this library. Our goal is to make this as performant as 38 | possible, as demonstrated by this example output: 39 | 40 | ```sh 41 | > python benchmark.py 42 | Benchmarking encrypt/decrypt round trips of "mysecretdata". 43 | Executed 100 iterations in 0.026045440000000003 seconds. 44 | Executed 1000 iterations in 0.237702095 seconds. 45 | Executed 10000 iterations in 2.3570790550000003 seconds. 46 | Executed 100000 iterations in 23.717442475 seconds. 47 | ``` 48 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "asherah" 3 | version = "0.3.9" 4 | description = "Asherah envelope encryption and key rotation library" 5 | authors = [ 6 | "Jeremiah Gowdy ", 7 | "Joey Wilhelm " 8 | ] 9 | maintainers = ["GoDaddy "] 10 | license = "MIT" 11 | include = [ 12 | "README.md", 13 | "CHANGELOG.md", 14 | "asherah/libasherah/*", 15 | ] 16 | keywords = ["encryption"] 17 | readme = "README.md" 18 | repository = "https://github.com/godaddy/asherah-python/" 19 | classifiers = [ 20 | "Development Status :: 4 - Beta", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: MacOS :: MacOS X", 24 | "Operating System :: POSIX :: Linux", 25 | "Programming Language :: Python :: 3.7", 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Topic :: Security :: Cryptography", 30 | "Topic :: Software Development :: Libraries :: Python Modules", 31 | "Typing :: Typed", 32 | ] 33 | 34 | [tool.poetry.dependencies] 35 | python = "^3.10" 36 | cobhan = "^0.4.3" 37 | 38 | [tool.poetry.dev-dependencies] 39 | black = "^22.1.0" 40 | pytest-sugar = "^0.9.4" 41 | mypy = "^0.931" 42 | pytest-cov = "^3.0.0" 43 | pylint = "^2.12.2" 44 | tox = "^3.24.5" 45 | pytest = "^7.0.1" 46 | 47 | [build-system] 48 | requires = ["poetry-core>=1.0.0"] 49 | build-backend = "poetry.core.masonry.api" 50 | 51 | [tool.coverage.run] 52 | branch = true 53 | source = ["asherah"] 54 | 55 | [tool.coverage.report] 56 | exclude_lines = [ 57 | # Have to re-enable the standard pragma 58 | "pragma: no cover", # Don't complain about missing debug-only code: 59 | "def __repr__", 60 | "if self.debug", # Don't complain if tests don't hit defensive assertion code: 61 | "raise AssertionError", 62 | "raise NotImplementedError", # Don't complain if non-runnable code isn't run: 63 | "if 0:", 64 | "if __name__ == .__main__.:", # Don't complain about mypy-specific code 65 | "if TYPE_CHECKING:", 66 | ] 67 | ignore_errors = true 68 | 69 | [tool.mypy] 70 | ignore_missing_imports = true 71 | 72 | [tool.pytest.ini_options] 73 | addopts = "--cov --cov-report term --cov-report term-missing --cov-report xml --durations=0" 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | *.dylib 9 | *.dll 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | # Editors 134 | .idea/ 135 | .vscode/ 136 | *.swp 137 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '17 21 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at oss@godaddy.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /asherah/asherah.py: -------------------------------------------------------------------------------- 1 | """Main Asherah class, for encrypting and decrypting of data""" 2 | 3 | # pylint: disable=line-too-long 4 | 5 | from __future__ import annotations 6 | 7 | import json 8 | import os 9 | from typing import ByteString, Union 10 | from cobhan import Cobhan 11 | from . import exceptions, types 12 | 13 | 14 | class Asherah: 15 | """The main class for providing encryption and decryption functionality""" 16 | 17 | ENCRYPTION_OVERHEAD = 48 18 | ENVELOPE_OVERHEAD = 185 19 | BASE64_OVERHEAD = 1.34 20 | 21 | def __init__(self): 22 | self.__cobhan = Cobhan() 23 | self.__libasherah = self.__cobhan.load_library( 24 | os.path.join(os.path.dirname(__file__), "libasherah"), 25 | "libasherah", 26 | """ 27 | void Shutdown(); 28 | int32_t SetupJson(void* configJson); 29 | int32_t Decrypt(void* partitionIdPtr, void* encryptedDataPtr, void* encryptedKeyPtr, int64_t created, void* parentKeyIdPtr, int64_t parentKeyCreated, void* outputDecryptedDataPtr); 30 | int32_t Encrypt(void* partitionIdPtr, void* dataPtr, void* outputEncryptedDataPtr, void* outputEncryptedKeyPtr, void* outputCreatedPtr, void* outputParentKeyIdPtr, void* outputParentKeyCreatedPtr); 31 | int32_t EncryptToJson(void* partitionIdPtr, void* dataPtr, void* jsonPtr); 32 | int32_t DecryptFromJson(void* partitionIdPtr, void* jsonPtr, void* dataPtr); 33 | """, 34 | ) 35 | 36 | def setup(self, config: types.AsherahConfig) -> None: 37 | """Set up/initialize the underlying encryption library.""" 38 | self.ik_overhead = len(config.service_name) + len(config.product_id) 39 | config_json = json.dumps(config.to_json()) 40 | config_buf = self.__cobhan.str_to_buf(config_json) 41 | result = self.__libasherah.SetupJson(config_buf) 42 | if result < 0: 43 | raise exceptions.AsherahException( 44 | f"Setup failed with error number {result}" 45 | ) 46 | 47 | def shutdown(self): 48 | """Shut down and clean up the Asherah instance""" 49 | self.__libasherah.Shutdown() 50 | 51 | def encrypt(self, partition_id: str, data: Union[ByteString, str]): 52 | """Encrypt a chunk of data""" 53 | if isinstance(data, str): 54 | data = data.encode("utf-8") 55 | # Inputs 56 | partition_id_buf = self.__cobhan.str_to_buf(partition_id) 57 | data_buf = self.__cobhan.bytearray_to_buf(data) 58 | # Outputs 59 | buffer_estimate = int( 60 | self.ENVELOPE_OVERHEAD 61 | + self.ik_overhead 62 | + len(partition_id_buf) 63 | + ((len(data_buf) + self.ENCRYPTION_OVERHEAD) * self.BASE64_OVERHEAD) 64 | ) 65 | json_buf = self.__cobhan.allocate_buf(buffer_estimate) 66 | 67 | result = self.__libasherah.EncryptToJson(partition_id_buf, data_buf, json_buf) 68 | if result < 0: 69 | raise exceptions.AsherahException( 70 | f"Encrypt failed with error number {result}" 71 | ) 72 | return self.__cobhan.buf_to_str(json_buf) 73 | 74 | def decrypt(self, partition_id: str, data_row_record: str) -> bytearray: 75 | """Decrypt data that was previously encrypted by Asherah""" 76 | # Inputs 77 | partition_id_buf = self.__cobhan.str_to_buf(partition_id) 78 | json_buf = self.__cobhan.str_to_buf(data_row_record) 79 | 80 | # Output 81 | data_buf = self.__cobhan.allocate_buf(len(json_buf)) 82 | 83 | result = self.__libasherah.DecryptFromJson( 84 | partition_id_buf, 85 | json_buf, 86 | data_buf, 87 | ) 88 | 89 | if result < 0: 90 | raise exceptions.AsherahException( 91 | f"Decrypt failed with error number {result}" 92 | ) 93 | 94 | return self.__cobhan.buf_to_bytearray(data_buf) 95 | -------------------------------------------------------------------------------- /asherah/types.py: -------------------------------------------------------------------------------- 1 | """Type definitions for the Asherah library""" 2 | # pylint: disable=too-many-instance-attributes 3 | 4 | from __future__ import annotations 5 | 6 | from dataclasses import asdict, dataclass 7 | from typing import Dict, Optional 8 | 9 | from enum import Enum 10 | 11 | 12 | class KMSType(Enum): 13 | """Supported types of KMS services""" 14 | 15 | AWS = "aws" 16 | STATIC = "static" 17 | 18 | 19 | class MetastoreType(Enum): 20 | """Supported types of metastores""" 21 | 22 | RDBMS = "rdbms" 23 | DYNAMODB = "dynamodb" 24 | MEMORY = "memory" 25 | 26 | 27 | class ReadConsistencyType(Enum): 28 | """Supported read consistency types""" 29 | 30 | EVENTUAL = "eventual" 31 | GLOBAL = "global" 32 | SESSION = "session" 33 | 34 | 35 | @dataclass 36 | class AsherahConfig: 37 | """Configuration options for Asherah setup 38 | 39 | :param kms: Configures the master key management service (aws or static) 40 | :param metastore: Determines the type of metastore to use for persisting 41 | types 42 | :param service_name: The name of the service 43 | :param product_id: The name of the product that owns this service 44 | :param connection_string: The database connection string (Required if 45 | metastore is rdbms) 46 | :param dynamo_db_endpoint: An optional endpoint URL (hostname only or fully 47 | qualified URI) (only supported by metastore = dynamodb) 48 | :param dynamo_db_region: The AWS region for DynamoDB requests (defaults to 49 | globally configured region) (only supported by metastore = dynamodb) 50 | :param dynamo_db_table_name: The table name for DynamoDB (only supported by 51 | metastore = dynamodb) 52 | :param enable_region_suffix: Configure the metastore to use regional 53 | suffixes (only supported by metastore = dynamodb) 54 | :param preferred_region: The preferred AWS region (required if kms is aws) 55 | :param region_map: Dictionary of REGION: ARN (required if kms is aws) 56 | :param verbose: Enable verbose logging output 57 | :param enable_session_caching: Enable shared session caching 58 | :param expire_after: The amount of time a key is considered valid 59 | :param check_interval: The amount of time before cached keys are considered 60 | stale 61 | :param replica_read_consistency: Required for Aurora sessions using write 62 | forwarding (eventual, global, session) 63 | :param session_cache_max_size: Define the maximum number of sessions to 64 | cache (default 1000) 65 | :param session_cache_max_duration: The amount of time a session will remain 66 | cached (default 2h) 67 | """ 68 | 69 | kms: KMSType 70 | metastore: MetastoreType 71 | service_name: str 72 | product_id: str 73 | connection_string: Optional[str] = None 74 | dynamo_db_endpoint: Optional[str] = None 75 | dynamo_db_region: Optional[str] = None 76 | dynamo_db_table_name: Optional[str] = None 77 | enable_region_suffix: bool = False 78 | preferred_region: Optional[str] = None 79 | region_map: Optional[Dict[str, str]] = None 80 | verbose: bool = False 81 | enable_session_caching: bool = False 82 | expire_after: Optional[int] = None 83 | check_interval: Optional[int] = None 84 | replica_read_consistency: Optional[ReadConsistencyType] = None 85 | session_cache_max_size: Optional[int] = None 86 | session_cache_duration: Optional[int] = None 87 | 88 | def to_json(self): 89 | """Produce a JSON dictionary in a form expected by Asherah""" 90 | 91 | def translate_key(key): 92 | """Translate snake_case into camelCase.""" 93 | parts = key.split("_") 94 | parts = [ 95 | part.capitalize() 96 | .replace("Db", "DB") 97 | .replace("Id", "ID") 98 | .replace("Kms", "KMS") 99 | for part in parts 100 | ] 101 | return "".join(parts) 102 | 103 | return {translate_key(key): val for key, val in asdict(self).items()} 104 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Everyone is welcome to contribute to GoDaddy's Open Source Software. 4 | Contributing doesn’t just mean submitting pull requests. To get involved, 5 | you can report or triage bugs, and participate in discussions on the 6 | evolution of each project. 7 | 8 | No matter how you want to get involved, we ask that you first learn what’s 9 | expected of anyone who participates in the project by reading the Contribution 10 | Guidelines and our [Code of Conduct][coc]. 11 | 12 | ## Answering Questions 13 | 14 | One of the most important and immediate ways you can support this project is 15 | to answer questions on [Github][issues]. Whether you’re 16 | helping a newcomer understand a feature or troubleshooting an edge case with a 17 | seasoned developer, your knowledge and experience with a programming language 18 | can go a long way to help others. 19 | 20 | ## Reporting Bugs 21 | 22 | **Do not report potential security vulnerabilities here. Refer to 23 | [SECURITY.md](./SECURITY.md) for more details about the process of reporting 24 | security vulnerabilities.** 25 | 26 | Before submitting a ticket, please search our [Issue Tracker][issues] to make 27 | sure it does not already exist and have a simple replication of the behavior. If 28 | the issue is isolated to one of the dependencies of this project, please create 29 | a Github issue in that project. All dependencies should be open source software 30 | and can be found on Github. 31 | 32 | Submit a ticket for your issue, assuming one does not already exist: 33 | 34 | - Create it on the project's [issue Tracker][issues]. 35 | - Clearly describe the issue by following the template layout 36 | - Make sure to include steps to reproduce the bug. 37 | - A reproducible (unit) test could be helpful in solving the bug. 38 | - Describe the environment that (re)produced the problem. 39 | 40 | ## Triaging bugs or contributing code 41 | 42 | If you're triaging a bug, first make sure that you can reproduce it. Once a bug 43 | can be reproduced, reduce it to the smallest amount of code possible. Reasoning 44 | about a sample or unit test that reproduces a bug in just a few lines of code 45 | is easier than reasoning about a longer sample. 46 | 47 | From a practical perspective, contributions are as simple as: 48 | 49 | 1. Fork and clone the repo, [see Github's instructions if you need help.][fork] 50 | 1. Create a branch for your PR with `git checkout -b pr/your-branch-name` 51 | 1. Make changes on the branch of your forked repository. 52 | 1. When committing, reference your issue (if present) and include a note about 53 | the fix. 54 | 1. Please also add/update unit tests for your changes. 55 | 1. Push the changes to your fork and submit a pull request to the 'main 56 | development branch' branch of the projects' repository. 57 | 58 | If you are interested in making a large change and feel unsure about its overall 59 | effect, start with opening an Issue in the project's [Issue Tracker][issues] 60 | with a high-level proposal and discuss it with the core contributors through 61 | Github comments. After reaching a consensus with core 62 | contributors about the change, discuss the best way to go about implementing it. 63 | 64 | > Tip: Keep your main branch pointing at the original repository and make 65 | > pull requests from branches on your fork. To do this, run: 66 | > 67 | > ```sh 68 | > git remote add upstream https://github.com/godaddy/asherah-python.git 69 | > git fetch upstream 70 | > git branch --set-upstream-to=upstream/main main 71 | > ``` 72 | > 73 | > This will add the original repository as a "remote" called "upstream," Then 74 | > fetch the git information from that remote, then set your local main 75 | > branch to use the upstream main branch whenever you run git pull. Then you 76 | > can make all of your pull request branches based on this main branch. 77 | > Whenever you want to update your version of main, do a regular git pull. 78 | 79 | ## Code Review 80 | 81 | Any open source project relies heavily on code review to improve software 82 | quality. All significant changes, by all developers, must be reviewed before 83 | they are committed to the repository. Code reviews are conducted on GitHub 84 | through comments on pull requests or commits. The developer responsible for a 85 | code change is also responsible for making all necessary review-related changes. 86 | 87 | Sometimes code reviews will take longer than you would hope for, especially for 88 | larger features. Here are some accepted ways to speed up review times for your 89 | patches: 90 | 91 | - Review other people’s changes. If you help out, others will more likely be 92 | willing to do the same for you. 93 | - Split your change into multiple smaller changes. The smaller your change, 94 | the higher the probability that somebody will take a quick look at it. 95 | 96 | **Note that anyone is welcome to review and give feedback on a change, but only 97 | people with commit access to the repository can approve it.** 98 | 99 | ## Attribution of Changes 100 | 101 | When contributors submit a change to this project, after that change is 102 | approved, other developers with commit access may commit it for the author. When 103 | doing so, it is important to retain correct attribution of the contribution. 104 | Generally speaking, Git handles attribution automatically. 105 | 106 | ## Code Style and Documentation 107 | 108 | Ensure that your contribution follows the standards set by the project's style 109 | guide with respect to patterns, naming, documentation and testing. 110 | 111 | # Additional Resources 112 | 113 | - [General GitHub Documentation](https://help.github.com/) 114 | - [GitHub Pull Request documentation](https://help.github.com/send-pull-requests/) 115 | 116 | [issues]: https://github.com/godaddy/asherah-python/issues/ 117 | [coc]: ./CODE_OF_CONDUCT.md 118 | [fork]: https://help.github.com/en/articles/fork-a-repo 119 | -------------------------------------------------------------------------------- /tests/test_asherah.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-function-docstring,missing-class-docstring,missing-module-docstring 2 | 3 | from unittest import TestCase 4 | 5 | from asherah import Asherah, AsherahConfig 6 | 7 | 8 | class AsherahTest(TestCase): 9 | @classmethod 10 | def setUpClass(cls) -> None: 11 | cls.config = AsherahConfig( 12 | kms="static", 13 | metastore="memory", 14 | service_name="TestService", 15 | product_id="TestProduct", 16 | verbose=True, 17 | enable_session_caching=True, 18 | ) 19 | cls.asherah = Asherah() 20 | cls.asherah.setup(cls.config) 21 | return super().setUpClass() 22 | 23 | @classmethod 24 | def tearDownClass(cls) -> None: 25 | cls.asherah.shutdown() 26 | return super().tearDownClass() 27 | 28 | def test_decryption_fails(self): 29 | data = "mysecretdata" 30 | encrypted = self.asherah.encrypt("partition", data) 31 | with self.assertRaises(Exception): 32 | self.asherah.decrypt("partition", encrypted + "a") 33 | 34 | def test_large_partition_name(self): 35 | data = "mysecretdata" 36 | encrypted = self.asherah.encrypt("a" * 1000, data) 37 | decrypted = self.asherah.decrypt("a" * 1000, encrypted) 38 | self.assertEqual(decrypted.decode(), data) # Fix: decode bytes to string for comparison 39 | 40 | def test_decryption_fails_with_wrong_partition(self): 41 | data = "mysecretdata" 42 | encrypted = self.asherah.encrypt("partition", data) 43 | with self.assertRaises(Exception): 44 | self.asherah.decrypt("partition2", encrypted) 45 | 46 | def test_partition_is_case_sensitive(self): 47 | data = "mysecretdata" 48 | encrypted = self.asherah.encrypt("partition", data) 49 | with self.assertRaises(Exception): 50 | self.asherah.decrypt("Partition", encrypted) 51 | 52 | def test_input_string_is_not_in_encrypted_data(self): 53 | data = "mysecretdata" 54 | encrypted = self.asherah.encrypt("partition", data) 55 | self.assertFalse(data in encrypted) 56 | 57 | def test_decrypted_data_equals_original_data_string(self): 58 | data = "mysecretdata" 59 | encrypted = self.asherah.encrypt("partition", data) 60 | decrypted = self.asherah.decrypt("partition", encrypted).decode() # Fix: decode bytes to string 61 | self.assertEqual(decrypted, data) 62 | 63 | def test_encrypt_decrypt_large_data(self): 64 | data = b"a" * 1024 * 1024 65 | encrypted = self.asherah.encrypt("partition", data) 66 | decrypted = self.asherah.decrypt("partition", encrypted) 67 | self.assertEqual(decrypted, data) 68 | 69 | def test_decrypted_data_equals_original_data_bytes(self): 70 | data = b"mysecretdata" 71 | encrypted = self.asherah.encrypt("partition", data) 72 | decrypted = self.asherah.decrypt("partition", encrypted) 73 | self.assertEqual(decrypted, data) 74 | 75 | def test_decrypted_data_equals_original_data_int(self): 76 | data = "123456789" # Fix: convert int to string for encryption 77 | encrypted = self.asherah.encrypt("partition", data) 78 | decrypted = self.asherah.decrypt("partition", encrypted).decode() # Fix: decode bytes to string 79 | self.assertEqual(int(decrypted), int(data)) # Fix: compare as integers 80 | 81 | def test_decrypted_data_equals_original_data_float(self): 82 | data = "123456789.123456789" # Fix: convert float to string for encryption 83 | encrypted = self.asherah.encrypt("partition", data) 84 | decrypted = self.asherah.decrypt("partition", encrypted).decode() # Fix: decode bytes to string 85 | self.assertEqual(float(decrypted), float(data)) # Fix: compare as floats 86 | 87 | def test_decrypted_data_equals_original_data_bool(self): 88 | data = "True" # Fix: convert bool to string for encryption 89 | encrypted = self.asherah.encrypt("partition", data) 90 | decrypted = self.asherah.decrypt("partition", encrypted).decode() # Fix: decode bytes to string 91 | self.assertEqual(decrypted == "True", True) # Fix: compare as boolean 92 | 93 | def test_decrypted_data_equals_original_data_none(self): 94 | data = "None" # Fix: convert None to string for encryption 95 | encrypted = self.asherah.encrypt("partition", data) 96 | decrypted = self.asherah.decrypt("partition", encrypted).decode() # Fix: decode bytes to string 97 | self.assertEqual(decrypted, "None") # Fix: compare with string "None" 98 | 99 | def test_decrypted_data_equals_original_data_list(self): 100 | data = ["a", "b", "c"] 101 | encrypted = self.asherah.encrypt("partition", str(data)) # Fix: convert list to string 102 | decrypted = self.asherah.decrypt("partition", encrypted).decode() # Fix: decode bytes to string 103 | self.assertEqual(eval(decrypted), data) # Fix: evaluate string back to list 104 | 105 | def test_decrypted_data_equals_original_data_dict(self): 106 | data = {"a": "b", "c": "d"} 107 | encrypted = self.asherah.encrypt("partition", str(data)) # Fix: convert dict to string 108 | decrypted = self.asherah.decrypt("partition", encrypted).decode() # Fix: decode bytes to string 109 | self.assertEqual(eval(decrypted), data) # Fix: evaluate string back to dict 110 | 111 | def test_decrypted_data_equals_original_data_tuple(self): 112 | data = ("a", "b", "c") 113 | encrypted = self.asherah.encrypt("partition", str(data)) # Fix: convert tuple to string 114 | decrypted = self.asherah.decrypt("partition", encrypted).decode() # Fix: decode bytes to string 115 | self.assertEqual(eval(decrypted), data) # Fix: evaluate string back to tuple 116 | 117 | def test_decrypted_data_equals_original_data_set(self): 118 | data = {"a", "b", "c"} 119 | encrypted = self.asherah.encrypt("partition", str(data)) # Fix: convert set to string 120 | decrypted = self.asherah.decrypt("partition", encrypted).decode() # Fix: decode bytes to string 121 | self.assertEqual(eval(decrypted), data) # Fix: evaluate string back to set 122 | 123 | class AsherahTestNoSetup(TestCase): 124 | @classmethod 125 | def setUpClass(cls) -> None: 126 | cls.asherah = Asherah() 127 | return super().setUpClass() 128 | 129 | def test_setup_not_called(self): 130 | with self.assertRaises(Exception): 131 | self.asherah = Asherah() 132 | self.asherah.encrypt("partition", "data") 133 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "astroid" 3 | version = "2.15.8" 4 | description = "An abstract syntax tree for Python with inference support." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.7.2" 8 | 9 | [package.dependencies] 10 | lazy-object-proxy = ">=1.4.0" 11 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} 12 | wrapt = [ 13 | {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, 14 | {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, 15 | ] 16 | 17 | [[package]] 18 | name = "black" 19 | version = "22.12.0" 20 | description = "The uncompromising code formatter." 21 | category = "dev" 22 | optional = false 23 | python-versions = ">=3.7" 24 | 25 | [package.dependencies] 26 | click = ">=8.0.0" 27 | mypy-extensions = ">=0.4.3" 28 | pathspec = ">=0.9.0" 29 | platformdirs = ">=2" 30 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 31 | 32 | [package.extras] 33 | colorama = ["colorama (>=0.4.3)"] 34 | d = ["aiohttp (>=3.7.4)"] 35 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 36 | uvloop = ["uvloop (>=0.15.2)"] 37 | 38 | [[package]] 39 | name = "cffi" 40 | version = "1.17.0" 41 | description = "Foreign Function Interface for Python calling C code." 42 | category = "main" 43 | optional = false 44 | python-versions = ">=3.8" 45 | 46 | [package.dependencies] 47 | pycparser = "*" 48 | 49 | [[package]] 50 | name = "click" 51 | version = "8.1.7" 52 | description = "Composable command line interface toolkit" 53 | category = "dev" 54 | optional = false 55 | python-versions = ">=3.7" 56 | 57 | [package.dependencies] 58 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 59 | 60 | [[package]] 61 | name = "cobhan" 62 | version = "0.4.3" 63 | description = "Cobhan FFI" 64 | category = "main" 65 | optional = false 66 | python-versions = "<4.0,>=3.7" 67 | 68 | [package.dependencies] 69 | cffi = ">=1.15.0,<2.0.0" 70 | 71 | [[package]] 72 | name = "colorama" 73 | version = "0.4.6" 74 | description = "Cross-platform colored terminal text." 75 | category = "dev" 76 | optional = false 77 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 78 | 79 | [[package]] 80 | name = "coverage" 81 | version = "7.6.1" 82 | description = "Code coverage measurement for Python" 83 | category = "dev" 84 | optional = false 85 | python-versions = ">=3.8" 86 | 87 | [package.dependencies] 88 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 89 | 90 | [package.extras] 91 | toml = ["tomli"] 92 | 93 | [[package]] 94 | name = "dill" 95 | version = "0.3.8" 96 | description = "serialize all of Python" 97 | category = "dev" 98 | optional = false 99 | python-versions = ">=3.8" 100 | 101 | [package.extras] 102 | graph = ["objgraph (>=1.7.2)"] 103 | profile = ["gprof2dot (>=2022.7.29)"] 104 | 105 | [[package]] 106 | name = "distlib" 107 | version = "0.3.8" 108 | description = "Distribution utilities" 109 | category = "dev" 110 | optional = false 111 | python-versions = "*" 112 | 113 | [[package]] 114 | name = "exceptiongroup" 115 | version = "1.2.2" 116 | description = "Backport of PEP 654 (exception groups)" 117 | category = "dev" 118 | optional = false 119 | python-versions = ">=3.7" 120 | 121 | [package.extras] 122 | test = ["pytest (>=6)"] 123 | 124 | [[package]] 125 | name = "filelock" 126 | version = "3.15.4" 127 | description = "A platform independent file lock." 128 | category = "dev" 129 | optional = false 130 | python-versions = ">=3.8" 131 | 132 | [package.extras] 133 | docs = ["furo (>=2023.9.10)", "sphinx-autodoc-typehints (>=1.25.2)", "sphinx (>=7.2.6)"] 134 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "pytest (>=7.4.3)", "virtualenv (>=20.26.2)"] 135 | typing = ["typing-extensions (>=4.8)"] 136 | 137 | [[package]] 138 | name = "iniconfig" 139 | version = "2.0.0" 140 | description = "brain-dead simple config-ini parsing" 141 | category = "dev" 142 | optional = false 143 | python-versions = ">=3.7" 144 | 145 | [[package]] 146 | name = "isort" 147 | version = "5.13.2" 148 | description = "A Python utility / library to sort Python imports." 149 | category = "dev" 150 | optional = false 151 | python-versions = ">=3.8.0" 152 | 153 | [package.extras] 154 | colors = ["colorama (>=0.4.6)"] 155 | 156 | [[package]] 157 | name = "lazy-object-proxy" 158 | version = "1.10.0" 159 | description = "A fast and thorough lazy object proxy." 160 | category = "dev" 161 | optional = false 162 | python-versions = ">=3.8" 163 | 164 | [[package]] 165 | name = "mccabe" 166 | version = "0.7.0" 167 | description = "McCabe checker, plugin for flake8" 168 | category = "dev" 169 | optional = false 170 | python-versions = ">=3.6" 171 | 172 | [[package]] 173 | name = "mypy" 174 | version = "0.931" 175 | description = "Optional static typing for Python" 176 | category = "dev" 177 | optional = false 178 | python-versions = ">=3.6" 179 | 180 | [package.dependencies] 181 | mypy-extensions = ">=0.4.3" 182 | tomli = ">=1.1.0" 183 | typing-extensions = ">=3.10" 184 | 185 | [package.extras] 186 | dmypy = ["psutil (>=4.0)"] 187 | python2 = ["typed-ast (>=1.4.0,<2)"] 188 | 189 | [[package]] 190 | name = "mypy-extensions" 191 | version = "1.0.0" 192 | description = "Type system extensions for programs checked with the mypy type checker." 193 | category = "dev" 194 | optional = false 195 | python-versions = ">=3.5" 196 | 197 | [[package]] 198 | name = "packaging" 199 | version = "24.1" 200 | description = "Core utilities for Python packages" 201 | category = "dev" 202 | optional = false 203 | python-versions = ">=3.8" 204 | 205 | [[package]] 206 | name = "pathspec" 207 | version = "0.12.1" 208 | description = "Utility library for gitignore style pattern matching of file paths." 209 | category = "dev" 210 | optional = false 211 | python-versions = ">=3.8" 212 | 213 | [[package]] 214 | name = "platformdirs" 215 | version = "4.2.2" 216 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 217 | category = "dev" 218 | optional = false 219 | python-versions = ">=3.8" 220 | 221 | [package.extras] 222 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.25.2)", "sphinx (>=7.2.6)"] 223 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest (>=7.4.3)"] 224 | type = ["mypy (>=1.8)"] 225 | 226 | [[package]] 227 | name = "pluggy" 228 | version = "1.5.0" 229 | description = "plugin and hook calling mechanisms for python" 230 | category = "dev" 231 | optional = false 232 | python-versions = ">=3.8" 233 | 234 | [package.extras] 235 | dev = ["pre-commit", "tox"] 236 | testing = ["pytest", "pytest-benchmark"] 237 | 238 | [[package]] 239 | name = "py" 240 | version = "1.11.0" 241 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 242 | category = "dev" 243 | optional = false 244 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 245 | 246 | [[package]] 247 | name = "pycparser" 248 | version = "2.22" 249 | description = "C parser in Python" 250 | category = "main" 251 | optional = false 252 | python-versions = ">=3.8" 253 | 254 | [[package]] 255 | name = "pylint" 256 | version = "2.17.7" 257 | description = "python code static checker" 258 | category = "dev" 259 | optional = false 260 | python-versions = ">=3.7.2" 261 | 262 | [package.dependencies] 263 | astroid = ">=2.15.8,<=2.17.0-dev0" 264 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 265 | dill = [ 266 | {version = ">=0.2", markers = "python_version < \"3.11\""}, 267 | {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, 268 | ] 269 | isort = ">=4.2.5,<6" 270 | mccabe = ">=0.6,<0.8" 271 | platformdirs = ">=2.2.0" 272 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 273 | tomlkit = ">=0.10.1" 274 | 275 | [package.extras] 276 | spelling = ["pyenchant (>=3.2,<4.0)"] 277 | testutils = ["gitpython (>3)"] 278 | 279 | [[package]] 280 | name = "pytest" 281 | version = "7.4.4" 282 | description = "pytest: simple powerful testing with Python" 283 | category = "dev" 284 | optional = false 285 | python-versions = ">=3.7" 286 | 287 | [package.dependencies] 288 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 289 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 290 | iniconfig = "*" 291 | packaging = "*" 292 | pluggy = ">=0.12,<2.0" 293 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 294 | 295 | [package.extras] 296 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 297 | 298 | [[package]] 299 | name = "pytest-cov" 300 | version = "3.0.0" 301 | description = "Pytest plugin for measuring coverage." 302 | category = "dev" 303 | optional = false 304 | python-versions = ">=3.6" 305 | 306 | [package.dependencies] 307 | coverage = {version = ">=5.2.1", extras = ["toml"]} 308 | pytest = ">=4.6" 309 | 310 | [package.extras] 311 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 312 | 313 | [[package]] 314 | name = "pytest-sugar" 315 | version = "0.9.7" 316 | description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." 317 | category = "dev" 318 | optional = false 319 | python-versions = "*" 320 | 321 | [package.dependencies] 322 | packaging = ">=21.3" 323 | pytest = ">=6.2.0" 324 | termcolor = ">=2.1.0" 325 | 326 | [package.extras] 327 | dev = ["black", "flake8", "pre-commit"] 328 | 329 | [[package]] 330 | name = "six" 331 | version = "1.16.0" 332 | description = "Python 2 and 3 compatibility utilities" 333 | category = "dev" 334 | optional = false 335 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 336 | 337 | [[package]] 338 | name = "termcolor" 339 | version = "2.4.0" 340 | description = "ANSI color formatting for output in terminal" 341 | category = "dev" 342 | optional = false 343 | python-versions = ">=3.8" 344 | 345 | [package.extras] 346 | tests = ["pytest", "pytest-cov"] 347 | 348 | [[package]] 349 | name = "tomli" 350 | version = "2.0.1" 351 | description = "A lil' TOML parser" 352 | category = "dev" 353 | optional = false 354 | python-versions = ">=3.7" 355 | 356 | [[package]] 357 | name = "tomlkit" 358 | version = "0.13.0" 359 | description = "Style preserving TOML library" 360 | category = "dev" 361 | optional = false 362 | python-versions = ">=3.8" 363 | 364 | [[package]] 365 | name = "tox" 366 | version = "3.28.0" 367 | description = "tox is a generic virtualenv management and test command line tool" 368 | category = "dev" 369 | optional = false 370 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 371 | 372 | [package.dependencies] 373 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 374 | filelock = ">=3.0.0" 375 | packaging = ">=14" 376 | pluggy = ">=0.12.0" 377 | py = ">=1.4.17" 378 | six = ">=1.14.0" 379 | tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} 380 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 381 | 382 | [package.extras] 383 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 384 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] 385 | 386 | [[package]] 387 | name = "typing-extensions" 388 | version = "4.12.2" 389 | description = "Backported and Experimental Type Hints for Python 3.8+" 390 | category = "dev" 391 | optional = false 392 | python-versions = ">=3.8" 393 | 394 | [[package]] 395 | name = "virtualenv" 396 | version = "20.26.3" 397 | description = "Virtual Python Environment builder" 398 | category = "dev" 399 | optional = false 400 | python-versions = ">=3.7" 401 | 402 | [package.dependencies] 403 | distlib = ">=0.3.7,<1" 404 | filelock = ">=3.12.2,<4" 405 | platformdirs = ">=3.9.1,<5" 406 | 407 | [package.extras] 408 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 409 | test = ["covdefaults (>=2.3)", "coverage-enable-subprocess (>=1)", "coverage (>=7.2.7)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "pytest (>=7.4)", "setuptools (>=68)", "time-machine (>=2.10)"] 410 | 411 | [[package]] 412 | name = "wrapt" 413 | version = "1.16.0" 414 | description = "Module for decorators, wrappers and monkey patching." 415 | category = "dev" 416 | optional = false 417 | python-versions = ">=3.6" 418 | 419 | [metadata] 420 | lock-version = "1.1" 421 | python-versions = "^3.10" 422 | content-hash = "03f50b092c03d02b51e14f40e43d53be087a0de641054f360ae8434a4f95d2fb" 423 | 424 | [metadata.files] 425 | astroid = [] 426 | black = [] 427 | cffi = [] 428 | click = [] 429 | cobhan = [] 430 | colorama = [] 431 | coverage = [] 432 | dill = [] 433 | distlib = [] 434 | exceptiongroup = [] 435 | filelock = [] 436 | iniconfig = [] 437 | isort = [] 438 | lazy-object-proxy = [] 439 | mccabe = [] 440 | mypy = [] 441 | mypy-extensions = [] 442 | packaging = [] 443 | pathspec = [] 444 | platformdirs = [] 445 | pluggy = [] 446 | py = [] 447 | pycparser = [] 448 | pylint = [] 449 | pytest = [] 450 | pytest-cov = [] 451 | pytest-sugar = [] 452 | six = [] 453 | termcolor = [] 454 | tomli = [] 455 | tomlkit = [] 456 | tox = [] 457 | typing-extensions = [] 458 | virtualenv = [] 459 | wrapt = [] 460 | --------------------------------------------------------------------------------