Repository: konidev20/pyshamir Branch: main Commit: 605ed8a538be Files: 31 Total size: 109.5 KB Directory structure: gitextract_tvko4ywd/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── dependabot.yml │ └── workflows/ │ ├── codeql.yml │ ├── compatibility-test.yml │ ├── release.yml │ ├── scorecard.yml │ └── unit-test-and-reporting.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CLAUDE.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── pyproject.toml ├── pyshamir/ │ ├── __init__.py │ ├── _utils.py │ └── shamir.py ├── requirements/ │ ├── build.in │ ├── build.txt │ ├── lint.in │ ├── lint.txt │ ├── test.in │ └── test.txt ├── tests/ │ ├── __init__.py │ ├── test_shamir.py │ ├── test_utils.py │ └── test_utils_properties.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" # Hash-pinned CI dependency files generated by pip-compile. - package-ecosystem: "pip" directory: "/requirements" schedule: interval: "weekly" ================================================ FILE: .github/workflows/codeql.yml ================================================ name: CodeQL on: push: branches: [main] pull_request: branches: [main] schedule: # Weekly Monday at 05:17 UTC. Offset from common cron times to avoid # GitHub's runner congestion at the top of the hour. - cron: '17 5 * * 1' permissions: contents: read jobs: analyze: name: Analyze (Python) runs-on: ubuntu-latest permissions: security-events: write # publish findings to the code-scanning dashboard actions: read # read workflow metadata for analysis tracing contents: read steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Initialize CodeQL uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: languages: python # security-extended adds queries beyond the default suite # (more findings, slightly slower scan; appropriate for a crypto lib). queries: security-extended - name: Perform CodeQL analysis uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: category: "/language:python" ================================================ FILE: .github/workflows/compatibility-test.yml ================================================ name: Compatibility Test on: workflow_dispatch: {} push: branches: - main pull_request: types: [opened, synchronize, reopened] permissions: contents: read jobs: compatibility: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} - name: Install package run: | python -m pip install --upgrade pip pip install . - name: Test import and basic functionality run: | python -c " import pyshamir print(f'PyShamir version: {pyshamir.__version__}') # Test basic functionality secret = b'Hello, World!' parts = pyshamir.split(secret, 5, 3) reconstructed = pyshamir.combine(parts[:3]) assert reconstructed == secret, 'Secret reconstruction failed' print('OK: Basic functionality test passed') print('OK: Package import and functionality verification completed successfully') " ================================================ FILE: .github/workflows/release.yml ================================================ # Manual release workflow for pyshamir. # # Dispatch this workflow with a target semver (e.g. 1.0.5, 1.1.0rc1) to: # 1. Validate the version (regex + not-equal-to-current + tag-doesn't-exist) # 2. Bump pyshamir/__init__.py and commit to main as github-actions[bot] # 3. Create and push the vX.Y.Z tag (atomic with the main push) # 4. Build sdist + wheel # 5. Publish to PyPI via OIDC trusted publishing # 6. Create a GitHub release with auto-generated release notes # # Restricted to the repository owner via `if: github.actor == github.repository_owner`. # # Rollback (if a release fails partway): # - PyPI publishes are immutable; if step 5 succeeded but step 6 failed, the # PyPI version exists and only the GitHub release page is missing — re-run # just the gh-release step manually, or create the release from the UI. # - If only step 2/3 succeeded (bump + tag pushed, no PyPI publish), delete # the bad tag (`git push --delete origin vX.Y.Z`) and revert the bump # commit on main, then re-dispatch. name: Release on: workflow_dispatch: inputs: version: description: 'Target version (semver, e.g. 1.0.5 or 1.1.0rc1). The vX.Y.Z tag is created automatically.' required: true type: string permissions: contents: read concurrency: group: release cancel-in-progress: false jobs: bump-and-tag: name: Bump version and tag runs-on: ubuntu-latest if: github.actor == github.repository_owner permissions: contents: write outputs: tag: ${{ steps.bump.outputs.tag }} steps: - name: Checkout main uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: main fetch-depth: 0 - name: Validate input env: VERSION: ${{ inputs.version }} run: | set -euo pipefail if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(rc[0-9]+|a[0-9]+|b[0-9]+)?$ ]]; then echo "::error::Version must match semver (e.g. 1.0.5 or 1.1.0rc1), got: '$VERSION'" exit 1 fi current=$(grep -E '^__version__\s*=' pyshamir/__init__.py | sed -E 's/.*"([^"]+)".*/\1/') if [[ "$VERSION" == "$current" ]]; then echo "::error::Version is unchanged from current ($current)" exit 1 fi if git ls-remote --tags --exit-code origin "refs/tags/v$VERSION" >/dev/null 2>&1; then echo "::error::Tag v$VERSION already exists on origin" exit 1 fi echo "Validation passed: bumping $current -> $VERSION" - name: Bump pyshamir/__init__.py id: bump env: VERSION: ${{ inputs.version }} run: | set -euo pipefail sed -i -E "s/^__version__\s*=.*/__version__ = \"$VERSION\"/" pyshamir/__init__.py echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" echo "--- pyshamir/__init__.py after bump ---" cat pyshamir/__init__.py - name: Commit, tag, and push atomically env: VERSION: ${{ inputs.version }} run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add pyshamir/__init__.py git commit -m "chore: bump version to v$VERSION" git tag "v$VERSION" # Atomic push: main commit and tag go together, or neither does. git push --atomic origin main "refs/tags/v$VERSION" build: name: Build distribution needs: bump-and-tag runs-on: ubuntu-latest steps: - name: Checkout the new tag uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ needs.bump-and-tag.outputs.tag }} - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.13" - name: Install build (hash-pinned) run: | python -m pip install --upgrade pip pip install --require-hashes -r requirements/build.txt - name: Build sdist + wheel run: python -m build - name: Upload dist artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist path: dist/ publish: name: Publish to PyPI via OIDC needs: build runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/pyshamir permissions: id-token: write steps: - name: Download dist artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: dist path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 gh-release: name: Create GitHub release needs: [bump-and-tag, build] runs-on: ubuntu-latest permissions: contents: write # for the release + asset upload id-token: write # sigstore keyless signing (Fulcio cert via OIDC) steps: - name: Download dist artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: dist path: dist/ - name: Sign sdist + wheel with Sigstore uses: sigstore/gh-action-sigstore-python@04cffa1d795717b140764e8b640de88853c92acc # v3.3.0 with: inputs: ./dist/*.tar.gz ./dist/*.whl - name: Create GitHub release with auto-generated notes uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: tag_name: ${{ needs.bump-and-tag.outputs.tag }} files: | dist/*.tar.gz dist/*.whl dist/*.sigstore.json generate_release_notes: true ================================================ FILE: .github/workflows/scorecard.yml ================================================ # This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy # policy, and support documentation. name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: '23 14 * * 0' push: branches: [ "main" ] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write # Uncomment the permissions below if installing in a private repository. # contents: read # actions: read steps: - name: "Checkout code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: sarif_file: results.sarif ================================================ FILE: .github/workflows/unit-test-and-reporting.yml ================================================ name: Unit Test & Reporting on: workflow_dispatch: {} push: branches: - main pull_request: types: [opened, synchronize, reopened] permissions: contents: read jobs: test: name: Test (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - name: Fetch files uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} - name: Install test dependencies (hash-pinned) run: | python -m pip install --upgrade pip pip install --require-hashes -r requirements/test.txt - name: Run pytest with coverage run: pytest --cov=pyshamir --cov-report=term-missing lint: name: Lint and type-check runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.13" - name: Install ruff and mypy (hash-pinned) run: | python -m pip install --upgrade pip pip install --require-hashes -r requirements/lint.txt - name: ruff check run: ruff check pyshamir tests - name: ruff format --check run: ruff format --check pyshamir tests - name: mypy --strict run: mypy pyshamir ================================================ FILE: .gitignore ================================================ # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,intellij,vim,emacs,git,python # Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,visualstudiocode,intellij,vim,emacs,git,python ### Emacs ### # -*- mode: gitignore; -*- *~ \#*\# /.emacs.desktop /.emacs.desktop.lock *.elc auto-save-list tramp .\#* # Org-mode .org-id-locations *_archive # flymake-mode *_flymake.* # eshell files /eshell/history /eshell/lastdir # elpa packages /elpa/ # reftex files *.rel # AUCTeX auto folder /auto/ # cask packages .cask/ dist/ # Flycheck flycheck_*.el # server auth directory /server/ # projectiles files .projectile # directory configuration .dir-locals.el # network security /network-security.data ### Git ### # Created by git for backups. To disable backups in Git: # $ git config --global mergetool.keepBackup false *.orig # Created by git when using merge tools for conflicts *.BACKUP.* *.BASE.* *.LOCAL.* *.REMOTE.* *_BACKUP_*.txt *_BASE_*.txt *_LOCAL_*.txt *_REMOTE_*.txt ### Intellij ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # AWS User-specific .idea/**/aws.xml # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # SonarLint plugin .idea/sonarlint/ # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ### Intellij Patch ### # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 # *.iml # modules.xml # .idea/misc.xml # *.ipr # Sonarlint plugin # https://plugins.jetbrains.com/plugin/7973-sonarlint .idea/**/sonarlint/ # SonarQube Plugin # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin .idea/**/sonarIssues.xml # Markdown Navigator plugin # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced .idea/**/markdown-navigator.xml .idea/**/markdown-navigator-enh.xml .idea/**/markdown-navigator/ # Cache file creation bug # See https://youtrack.jetbrains.com/issue/JBR-2257 .idea/$CACHE_FILE$ # CodeStream plugin # https://plugins.jetbrains.com/plugin/12206-codestream .idea/codestream.xml # Azure Toolkit for IntelliJ plugin # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij .idea/**/azureSettings.xml ### Linux ### # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### macOS Patch ### # iCloud generated files *.icloud ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ ### Vim ### # Swap [._]*.s[a-v][a-z] !*.svg # comment out if you don't need vector files [._]*.sw[a-p] [._]s[a-rt-v][a-z] [._]ss[a-gi-z] [._]sw[a-p] # Session Session.vim Sessionx.vim # Temporary .netrwhist # Auto-generated tag files tags # Persistent undo [._]*.un~ ### VisualStudioCode ### .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets # Local History for Visual Studio Code .history/ # Built Visual Studio Code Extensions *.vsix ### VisualStudioCode Patch ### # Ignore all local history of files .history .ionide ### Windows ### # Windows thumbnail cache files Thumbs.db Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,intellij,vim,emacs,git,python ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-toml - id: check-merge-conflict - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.12 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.20.2 hooks: - id: mypy files: ^pyshamir/ args: [--strict] ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Python port of HashiCorp Vault's `shamir` package (Go) implementing Shamir's Secret Sharing. The public API is just `split(secret, parts, threshold)` and `combine(parts)`, exported from `pyshamir/__init__.py`. When behavior of the algorithm is ambiguous, the Go original is the source of truth: https://github.com/hashicorp/vault/tree/main/shamir. Match its semantics rather than inventing new ones. ## Commands Tests use `pytest` with `pytest-cov`. Lint is `ruff` (check + format), type-check is `mypy --strict`. All tool config lives in `pyproject.toml`. ```sh # Run all tests pytest # Run a single test pytest tests/test_utils.py::test_mul_known_vectors # Run tests with coverage pytest --cov=pyshamir --cov-report=term-missing # Lint + format check ruff check pyshamir tests ruff format --check pyshamir tests # Type-check (strict) mypy pyshamir # Multi-version matrix via tox tox # py39..py313 + lint + type tox -e py311 # single Python tox -e lint # ruff only tox -e type # mypy only tox -e coverage # writes coverage.xml + htmlcov/ ``` Pre-commit hooks (ruff, ruff-format, mypy --strict, plus standard whitespace/yaml hooks) are configured in `.pre-commit-config.yaml`. Install with `pre-commit install` after cloning. ## Architecture Two-file core; treat them as a unit when changing the algorithm. **`pyshamir/shamir.py`** — public `split` / `combine`. Wire format: each "part" is a `bytearray` of length `len(secret) + 1`; the **final byte** holds the x-coordinate (offset by +1 so it's never 0), and bytes `0..len(secret)-1` hold the polynomial evaluations at that x for each secret byte. `combine` reverses this by reading the last byte as x, then Lagrange-interpolating each byte position back to x=0. Both `parts` and `threshold` must be ≥2 and ≤255. **`pyshamir/_utils.py`** — GF(256) finite field arithmetic and the `Polynomial` class. `add` is XOR; `mul` implements the Rijndael/AES reduction polynomial (0x1B) and uses `ctypes.c_uint8` for exact 8-bit wraparound that matches the Go reference. `inverse` is a fixed exponentiation chain (a^254). `make_polynomial` builds a degree-`(threshold-1)` polynomial with a fixed intercept (the secret byte) and **cryptographically random coefficients via `secrets.token_bytes`** — do not swap to `random` or any non-CSPRNG source. `generate_x_coordinates` likewise uses `secrets.SystemRandom().shuffle`. `Polynomial.evaluate` uses Horner's method over GF(256). `interpolate_polynomial` is Lagrange interpolation in GF(256) — note `add` is XOR so `add(a, b)` doubles as both addition and subtraction. `tests/test_utils.py` exercises the GF(256) primitives directly (FIPS 197 worked vectors, Rijndael overflow, the `inverse(0)` edge case, a make/sample/interpolate round-trip). When changing math in `_utils.py`, run that file specifically — failures land closer to the broken primitive than they would going through `split`/`combine`. ## Conventions - **PEP-585 generic type hints are allowed** (`list[bytearray]`, `dict[int, bool]`). They work under py3.9 because both source files start with `from __future__ import annotations`, which defers all annotation evaluation to strings. Don't add them to a file that's missing the `__future__` import without also adding the import. - **`mypy --strict` must stay green.** New functions need full type annotations; new module-level re-exports need an `__all__` entry in `pyshamir/__init__.py`. - **Don't reword exception messages.** Tests in `tests/test_shamir.py` match them with `pytest.raises(..., match=...)`; changing an error string will break tests in another file. - **Version source**: `__version__` in `pyshamir/__init__.py` is read at build time by `pyproject.toml`'s `[tool.setuptools.dynamic] version = { attr = ... }`. Bumping the version is a one-line change there; tag release commits as `chore: bump version to vX.Y.Z`. - **License is MPL-2.0** (since commit 983d9f1). The SPDX expression in `pyproject.toml` and the matching `LICENSE` file are the canonical source — don't add `License :: ...` classifiers (PEP 639 reserves them as mutually exclusive with the SPDX form). ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at sgovind.dev@outlook.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # How to contribute Please read these guidelines before contributing to pyshamir: - [Question or Problem?](#question) - [Issues and Bugs](#issue) - [Submitting a Pull Request](#pullrequest) - [Contributor License Agreement](#cla) ## Got a Question or Problem? You can start a discussion thread on GitHub Discussions linked with this library found at [pyshamir Discussions](https://github.com/konidev20/pyshamir/discussions) GitHub issues are only for [reporting bugs](#issue) and [feature requests](#feature), not questions or help. ## Found an Issue? If you find a bug in the source code or a mistake in the documentation, you can help by submitting an issue to the [GitHub Repository][github]. Even better you can submit a Pull Request with a fix. When submitting an issue please include the following information: - A description of the issue which includes the pyshamir version and Python version - The exception message - If possible, please include code that reproduces the issue. [DropBox][dropbox] or GitHub's [Gist][gist] can be used to share large code samples, or you could [submit a pull request](#pullrequest) with the issue reproduced in a new test. The more information you include about the issue, the more likely it is to be fixed! ## Submitting a Pull Request When submitting a pull request to the [GitHub Repository][github] make sure to do the following: - Please adhere to the existing coding pattern in the project. It should be straight-forward to absorb. - Ensure that the unit test coverage is more than 80% - Write appropriate code samples in the documentation. (README.md) - Add the summary of the issue in the changelog Read [GitHub Help][pullrequesthelp] for more details about creating pull requests. ## Contributor License Agreement **Thank you for submitting your contributions to this project.** By signing this CLA, you agree that the following terms apply to all of your past, present and future contributions to the project. ### License. You hereby represent that all present, past and future contributions are governed by the [Mozilla Public License 2.0](https://opensource.org/licenses/MPL-2.0) copyright statement. This entails that to the extent possible under law, you transfer all copyright and related or neighboring rights of the code or documents you contribute to the project itself or its maintainers. Furthermore you also represent that you have the authority to perform the above waiver with respect to the entirety of you contributions. ### Moral Rights. To the fullest extent permitted under applicable law, you hereby waive, and agree not to assert, all of your “moral rights” in or relating to your contributions for the benefit of the project. ### Third Party Content. If your Contribution includes or is based on any source code, object code, bug fixes, configuration changes, tools, specifications, documentation, data, materials, feedback, information or other works of authorship that were not authored by you (“Third Party Content”) or if you are aware of any third party intellectual property or proprietary rights associated with your Contribution (“Third Party Rights”), then you agree to include with the submission of your Contribution full details respecting such Third Party Content and Third Party Rights, including, without limitation, identification of which aspects of your Contribution contain Third Party Content or are associated with Third Party Rights, the owner/author of the Third Party Content and Third Party Rights, where you obtained the Third Party Content, and any applicable third party license terms or restrictions respecting the Third Party Content and Third Party Rights. For greater certainty, the foregoing obligations respecting the identification of Third Party Content and Third Party Rights do not apply to any portion of a Project that is incorporated into your Contribution to that same Project. ### Representations. You represent that, other than the Third Party Content and Third Party Rights identified by you in accordance with this Agreement, you are the sole author of your Contributions and are legally entitled to grant the foregoing licenses and waivers in respect of your Contributions. If your Contributions were created in the course of your employment with your past or present employer(s), you represent that such employer(s) has authorized you to make your Contributions on behalf of such employer(s) or such employer (s) has waived all of their right, title or interest in or to your Contributions. ### Disclaimer. To the fullest extent permitted under applicable law, your Contributions are provided on an "as is" basis, without any warranties or conditions, express or implied, including, without limitation, any implied warranties or conditions of non-infringement, merchantability or fitness for a particular purpose. You are not required to provide support for your Contributions, except to the extent you desire to provide support. ### No Obligation. You acknowledge that the maintainers of this project are under no obligation to use or incorporate your contributions into the project. The decision to use or incorporate your contributions into the project will be made at the sole discretion of the maintainers or their authorized delegates. [github]: https://github.com/konidev20/pyshamir [dropbox]: https://www.dropbox.com [gist]: https://gist.github.com [pullrequesthelp]: https://help.github.com/articles/using-pull-requests ================================================ FILE: LICENSE ================================================ Copyright (c) 2015 HashiCorp, Inc. Mozilla Public License, version 2.0 1. Definitions 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means a. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or b. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: a. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or b. any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: a. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and b. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: a. for any code that a Contributor has removed from Covered Software; or b. for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or c. under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: a. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and b. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an "as is" basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: README.md ================================================ ![pyshamir banner](https://user-images.githubusercontent.com/5201843/232241639-22034903-87c2-4bf0-9b36-2ae9a8481b71.png) ## Description Python port of Shamir key Split and Combine methods from Hashicorp Vault. ## Requirements - Python 3.9 or higher ## Installation ```sh pip install pyshamir ``` ## Usage ### Split & Combine ```py from pyshamir import split, combine import secrets # generate a random secret, here secret is a 32 bytes secret = secrets.token_bytes(32) # set the number of shares; i.e. the number of parts to split the secret into num_of_shares = 5 # threshold is minimum number of keys required to get back the secret threshold = 3 # split to get a list of bytearrays which can be combined later to get back the secret parts = split(secret, num_of_shares, threshold) # Now, the parts be combined to get back the secret recomb_secret = combine(parts) ``` ## References 1. [Shamir Secret Sharing | Wikipedia](https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing) 2. [Go Implementation | HashiCorp Vault](https://github.com/hashicorp/vault/tree/main/shamir) ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | 1.0.x | :white_check_mark: | ## Reporting a Vulnerability Please report security vulnerabilities privately via GitHub's [private vulnerability reporting](https://github.com/konidev20/pyshamir/security/advisories/new) so the issue can be triaged and patched before public disclosure. Do **not** open a public issue for security reports. ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools>=77.0.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "pyshamir" description = "Python port of Shamir key Split and Combine methods from Hashicorp Vault." readme = "README.md" license = "MPL-2.0" license-files = ["LICENSE"] requires-python = ">=3.9" authors = [{ name = "Srigovind Nayak", email = "sgovind.dev@outlook.com" }] maintainers = [{ name = "Srigovind Nayak", email = "sgovind.dev@outlook.com" }] keywords = ["shamir", "pyshamir"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Security :: Cryptography", "Topic :: Software Development :: Libraries :: Python Modules", ] dynamic = ["version"] [project.optional-dependencies] tests = ["pytest", "pytest-cov", "hypothesis"] [project.urls] Homepage = "https://github.com/konidev20/pyshamir" Documentation = "https://github.com/konidev20/pyshamir" "Source Code" = "https://github.com/konidev20/pyshamir" "Issue Tracker" = "https://github.com/konidev20/pyshamir/issues" [tool.setuptools] packages = ["pyshamir"] [tool.setuptools.dynamic] version = { attr = "pyshamir.__version__" } [tool.ruff] line-length = 88 target-version = "py39" [tool.ruff.lint] select = ["E", "F", "I", "UP", "B"] extend-ignore = ["E203"] [tool.mypy] python_version = "3.9" strict = true files = ["pyshamir"] [tool.pytest.ini_options] testpaths = ["tests"] addopts = "--strict-markers" ================================================ FILE: pyshamir/__init__.py ================================================ from .shamir import combine, split __all__ = ["combine", "split"] __version__ = "1.0.4" ================================================ FILE: pyshamir/_utils.py ================================================ from __future__ import annotations import secrets from ctypes import c_uint8 def add(a: int, b: int) -> int: """ Adds two numbers in the finite field GF(256) """ out = a ^ b return out def inverse(a: int) -> int: b = mul(a, a) c = mul(a, b) b = mul(c, c) b = mul(b, b) c = mul(b, c) b = mul(b, b) b = mul(b, b) b = mul(b, c) b = mul(b, b) b = mul(a, b) return mul(b, b) def mul(a: int, b: int) -> int: a_u = c_uint8(a) b_u = c_uint8(b) r = c_uint8(0) i = 8 while i > 0: i -= 1 p1 = -(c_uint8(b_u.value >> i).value & 1) & a_u.value p2 = -(c_uint8(r.value >> 7).value) & 0x1B p3 = c_uint8(2 * r.value) r = c_uint8(p1 ^ p2 ^ p3.value) return r.value def div(a: int, b: int) -> int: a_u = c_uint8(a) b_u = c_uint8(b) if b_u.value == 0: raise ZeroDivisionError("Divide by zero") ret = mul(a_u.value, inverse(b_u.value)) if a_u.value == 0: return 0 return ret class Polynomial: """Polynomial in GF(256), evaluated via Horner's method.""" def __init__(self, degree: int) -> None: self.coefficients = bytearray(degree + 1) def evaluate(self, x: int) -> int: # origin case if x == 0: return self.coefficients[0] # compute the polynomial value using Horner's method degree = len(self.coefficients) - 1 out = self.coefficients[degree] for i in range(degree - 1, -1, -1): coeff = self.coefficients[i] out = add(mul(out, x), coeff) return out def make_polynomial(intercept: int, degree: int) -> Polynomial: """ Creates a random polynomial with the given intercept and degree :param intercept: :param degree: :return: """ polynomial_instance = Polynomial(degree) # Set the intercept polynomial_instance.coefficients[0] = intercept # assign random co-efficients to the polynomial polynomial_instance.coefficients[1:] = secrets.token_bytes(degree) return polynomial_instance def interpolate_polynomial(x_samples: bytearray, y_samples: bytearray, x: int) -> int: """Lagrange-interpolate N sample points in GF(256), returning the value at x.""" limit = len(x_samples) result = 0 for i in range(limit): basis = 1 for j in range(limit): if i != j: num = add(x, x_samples[j]) den = add(x_samples[i], x_samples[j]) term = div(num, den) basis = mul(basis, term) group = mul(y_samples[i], basis) result = add(result, group) return result def generate_x_coordinates(n: int) -> list[int]: x_coordinates = list(range(n)) secrets.SystemRandom().shuffle(x_coordinates) return x_coordinates ================================================ FILE: pyshamir/shamir.py ================================================ from __future__ import annotations from ._utils import generate_x_coordinates, interpolate_polynomial, make_polynomial def combine(parts: list[bytearray]) -> bytearray: """ Takes a list of parts and returns the secret :param parts: :return: """ # Verify enough parts are present if parts is None: raise ValueError("Not enough parts to combine") if len(parts) < 2: raise ValueError("Not enough parts to combine") # Verify all parts are all the same length first_part_len = len(parts[0]) if first_part_len < 2: raise ValueError("Part is too short") for part in parts: if len(part) != first_part_len: raise ValueError("Parts are not the same length") # Create a buffer to store the reconstructed secret secret = bytearray(first_part_len - 1) # Buffer to store the samples x_samples = bytearray(len(parts)) y_samples = bytearray(len(parts)) # Record x for each sample; duplicate x-coordinates would break interpolation. check_map: dict[int, bool] = {} for i, part in enumerate(parts): samp = part[first_part_len - 1] if samp in check_map: raise ValueError("Duplicate sample") check_map[samp] = True x_samples[i] = samp # Reconstruct each byte for idx, _ in enumerate(secret): for i, part in enumerate(parts): y_samples[i] = part[idx] # interpolate the polynomial and compute the vault at 0 val = interpolate_polynomial(x_samples, y_samples, 0) # Evaluate the 0th value to get the intercept secret[idx] = val return secret def split(secret: bytes, parts: int, threshold: int) -> list[bytearray]: """ Takes a secret and splits it into parts :param secret: :param parts: :param threshold: :return: """ # Sanity check the input if parts < 2 or threshold < 2: raise ValueError("Parts and threshold must be greater than 1") if parts < threshold: raise ValueError("Parts must be greater than threshold") if parts > 255: raise ValueError("Parts must be less than 256") if secret is None: raise ValueError("Secret must be at least 1 byte long") if len(secret) < 1: raise ValueError("Secret must be at least 1 byte long") # Generate random list of x coordinates x_coordinates = generate_x_coordinates(255) # Allocate output buffers; the final byte of each holds the (offset) x-coordinate. # Random x's ensure repeated splits of the same secret yield different parts. output = [bytearray() for _ in range(parts)] for i in range(len(output)): output[i] = bytearray(len(secret) + 1) output[i][len(secret)] = int(x_coordinates[i]) + 1 for i, val in enumerate(secret): polynomial_instance = make_polynomial(val, int(threshold - 1)) for j in range(parts): x = int(x_coordinates[j]) + 1 y = polynomial_instance.evaluate(x) output[j][i] = y return output ================================================ FILE: requirements/build.in ================================================ build ================================================ FILE: requirements/build.txt ================================================ # This file was autogenerated by uv via the following command: # uv pip compile --python-version 3.9 --generate-hashes --strip-extras --output-file=build.txt build.in build==1.4.4 \ --hash=sha256:8c3f48a6090b39edec1a273d2d57949aaf13723b01e02f9d518396887519f64d \ --hash=sha256:f832ae053061f3fb524af812dc94b8b84bac6880cd587630e3b5d91a6a9c1703 # via -r build.in importlib-metadata==8.7.1 \ --hash=sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb \ --hash=sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151 # via build packaging==26.2 \ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via build pyproject-hooks==1.2.0 \ --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 # via build tomli==2.4.1 \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via build zipp==3.23.1 \ --hash=sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc \ --hash=sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110 # via importlib-metadata ================================================ FILE: requirements/lint.in ================================================ ruff mypy ================================================ FILE: requirements/lint.txt ================================================ # This file was autogenerated by uv via the following command: # uv pip compile --python-version 3.9 --generate-hashes --strip-extras --output-file=lint.txt lint.in librt==0.9.0 \ --hash=sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d \ --hash=sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d \ --hash=sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38 \ --hash=sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8 \ --hash=sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a \ --hash=sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb \ --hash=sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6 \ --hash=sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499 \ --hash=sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2 \ --hash=sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285 \ --hash=sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5 \ --hash=sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0 \ --hash=sha256:2d03fa4fd277a7974c1978c92c374c57f44edeee163d147b477b143446ad1bf6 \ --hash=sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443 \ --hash=sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b \ --hash=sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745 \ --hash=sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb \ --hash=sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228 \ --hash=sha256:42ff8a962554c350d4a83cf47d9b7b78b0e6ff7943e87df7cdfc97c07f3c016f \ --hash=sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c \ --hash=sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845 \ --hash=sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5 \ --hash=sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c \ --hash=sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f \ --hash=sha256:5112c2fb7c2eefefaeaf5c97fec81343ef44ee86a30dcfaa8223822fba6467b4 \ --hash=sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54 \ --hash=sha256:54d412e47c21b85865676ed0724e37a89e9593c2eee1e7367adf85bfad56ffb1 \ --hash=sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236 \ --hash=sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f \ --hash=sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27 \ --hash=sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b \ --hash=sha256:657f8ba7b9eaaa82759a104137aed2a3ef7bc46ccfd43e0d89b04005b3e0a4cc \ --hash=sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858 \ --hash=sha256:6788207daa0c19955d2b668f3294a368d19f67d9b5f274553fd073c1260cbb9f \ --hash=sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b \ --hash=sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938 \ --hash=sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a \ --hash=sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b \ --hash=sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d \ --hash=sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f \ --hash=sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71 \ --hash=sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22 \ --hash=sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8 \ --hash=sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990 \ --hash=sha256:81107843ed1836874b46b310f9b1816abcb89912af627868522461c3b7333c0f \ --hash=sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2 \ --hash=sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd \ --hash=sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076 \ --hash=sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671 \ --hash=sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9 \ --hash=sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15 \ --hash=sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4 \ --hash=sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f \ --hash=sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8 \ --hash=sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d \ --hash=sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265 \ --hash=sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61 \ --hash=sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519 \ --hash=sha256:a81eea9b999b985e4bacc650c4312805ea7008fd5e45e1bf221310176a7bcb3a \ --hash=sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40 \ --hash=sha256:aa95738a68cedd3a6f5492feddc513e2e166b50602958139e47bbdd82da0f5a7 \ --hash=sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f \ --hash=sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e \ --hash=sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9 \ --hash=sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a \ --hash=sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3 \ --hash=sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee \ --hash=sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11 \ --hash=sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4 \ --hash=sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283 \ --hash=sha256:d9da80e5b04acce03ced8ba6479a71c2a2edf535c2acc0d09c80d2f80f3bad15 \ --hash=sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084 \ --hash=sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e \ --hash=sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882 \ --hash=sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f \ --hash=sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e \ --hash=sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774 \ --hash=sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce \ --hash=sha256:eea1b54943475f51698f85fa230c65ccac769f1e603b981be060ac5763d90927 \ --hash=sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6 \ --hash=sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118 \ --hash=sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4 \ --hash=sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e \ --hash=sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1 \ --hash=sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f \ --hash=sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2 \ --hash=sha256:f48c963a76d71b9d7927eb817b543d0dccd52ab6648b99d37bd54f4cd475d856 \ --hash=sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1 \ --hash=sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f \ --hash=sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a # via mypy mypy==1.19.1 \ --hash=sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd \ --hash=sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b \ --hash=sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1 \ --hash=sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba \ --hash=sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b \ --hash=sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045 \ --hash=sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac \ --hash=sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6 \ --hash=sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a \ --hash=sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24 \ --hash=sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957 \ --hash=sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042 \ --hash=sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e \ --hash=sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec \ --hash=sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3 \ --hash=sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718 \ --hash=sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f \ --hash=sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331 \ --hash=sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1 \ --hash=sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1 \ --hash=sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13 \ --hash=sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67 \ --hash=sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2 \ --hash=sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a \ --hash=sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b \ --hash=sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8 \ --hash=sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376 \ --hash=sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef \ --hash=sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288 \ --hash=sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75 \ --hash=sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74 \ --hash=sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250 \ --hash=sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab \ --hash=sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6 \ --hash=sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247 \ --hash=sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925 \ --hash=sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e \ --hash=sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e # via -r lint.in mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 # via mypy pathspec==1.1.0 \ --hash=sha256:574b128f7456bd899045ccd142dd446af7e6cfd0072d63ad73fbc55fbb4aaa42 \ --hash=sha256:f5d7c555da02fd8dde3e4a2354b6aba817a89112fa8f333f7917a2a4834dd080 # via mypy ruff==0.15.12 \ --hash=sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b \ --hash=sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33 \ --hash=sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0 \ --hash=sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002 \ --hash=sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339 \ --hash=sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e \ --hash=sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847 \ --hash=sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f \ --hash=sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6 \ --hash=sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d \ --hash=sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20 \ --hash=sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd \ --hash=sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c \ --hash=sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5 \ --hash=sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6 \ --hash=sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c \ --hash=sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5 \ --hash=sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5 # via -r lint.in tomli==2.4.1 \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via mypy typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via mypy ================================================ FILE: requirements/test.in ================================================ pytest pytest-cov hypothesis ================================================ FILE: requirements/test.txt ================================================ # This file was autogenerated by uv via the following command: # uv pip compile --python-version 3.9 --generate-hashes --strip-extras --output-file=test.txt test.in attrs==26.1.0 \ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via hypothesis coverage==7.10.7 \ --hash=sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9 \ --hash=sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880 \ --hash=sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999 \ --hash=sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1 \ --hash=sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13 \ --hash=sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b \ --hash=sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82 \ --hash=sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973 \ --hash=sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f \ --hash=sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681 \ --hash=sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0 \ --hash=sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541 \ --hash=sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32 \ --hash=sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17 \ --hash=sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a \ --hash=sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40 \ --hash=sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd \ --hash=sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6 \ --hash=sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7 \ --hash=sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb \ --hash=sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f \ --hash=sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d \ --hash=sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe \ --hash=sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c \ --hash=sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807 \ --hash=sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab \ --hash=sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2 \ --hash=sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546 \ --hash=sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e \ --hash=sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65 \ --hash=sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396 \ --hash=sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431 \ --hash=sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb \ --hash=sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699 \ --hash=sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0 \ --hash=sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f \ --hash=sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a \ --hash=sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235 \ --hash=sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911 \ --hash=sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23 \ --hash=sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87 \ --hash=sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699 \ --hash=sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a \ --hash=sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b \ --hash=sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256 \ --hash=sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a \ --hash=sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417 \ --hash=sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0 \ --hash=sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a \ --hash=sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360 \ --hash=sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0 \ --hash=sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b \ --hash=sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb \ --hash=sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2 \ --hash=sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d \ --hash=sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a \ --hash=sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e \ --hash=sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69 \ --hash=sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14 \ --hash=sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d \ --hash=sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f \ --hash=sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2 \ --hash=sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c \ --hash=sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0 \ --hash=sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399 \ --hash=sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59 \ --hash=sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63 \ --hash=sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b \ --hash=sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2 \ --hash=sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e \ --hash=sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0 \ --hash=sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520 \ --hash=sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df \ --hash=sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c \ --hash=sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b \ --hash=sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2 \ --hash=sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f \ --hash=sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61 \ --hash=sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a \ --hash=sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59 \ --hash=sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c \ --hash=sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf \ --hash=sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07 \ --hash=sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6 \ --hash=sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e \ --hash=sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594 \ --hash=sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49 \ --hash=sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843 \ --hash=sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14 \ --hash=sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3 \ --hash=sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1 \ --hash=sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698 \ --hash=sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15 \ --hash=sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d \ --hash=sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5 \ --hash=sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e \ --hash=sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0 \ --hash=sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b \ --hash=sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239 \ --hash=sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba \ --hash=sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4 \ --hash=sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260 \ --hash=sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a \ --hash=sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3 # via pytest-cov exceptiongroup==1.3.1 \ --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 # via # hypothesis # pytest hypothesis==6.141.1 \ --hash=sha256:8ef356e1e18fbeaa8015aab3c805303b7fe4b868e5b506e87ad83c0bf951f46f \ --hash=sha256:a5b3c39c16d98b7b4c3c5c8d4262e511e3b2255e6814ced8023af49087ad60b3 # via -r test.in iniconfig==2.1.0 \ --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \ --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 # via pytest packaging==26.2 \ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via pytest pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via # pytest # pytest-cov pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via pytest pytest==8.4.2 \ --hash=sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01 \ --hash=sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79 # via # -r test.in # pytest-cov pytest-cov==7.1.0 \ --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 # via -r test.in sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via hypothesis tomli==2.4.1 \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via # coverage # pytest typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via exceptiongroup ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/test_shamir.py ================================================ from __future__ import annotations import itertools from base64 import b64decode import pytest from pyshamir import combine, split SPLIT_SECRET = b64decode("a+m4G0kEkKDQK4MFGz7L0vLz5oViQkDSLThiC4zDRZU=") COMBINE_SECRET = b64decode("esfX3MUC++BrcwkiwsKtK6M5Pi5yvuc/A/6LweWJ5FA=") def test_split_returns_correct_part_count_and_length(): parts = split(SPLIT_SECRET, 5, 3) assert len(parts) == 5 for part in parts: assert len(part) == len(SPLIT_SECRET) + 1 def test_split_produces_distinct_parts(): parts = split(SPLIT_SECRET, 5, 3) for i in range(len(parts) - 1): assert parts[i].hex() != parts[i + 1].hex() @pytest.mark.parametrize( "parts, threshold, message", [ (0, 0, "Parts and threshold must be greater than 1"), (2, 3, "Parts must be greater than threshold"), (1000, 3, "Parts must be less than 256"), ], ) def test_split_invalid_arguments(parts, threshold, message): with pytest.raises(ValueError, match=message): split(SPLIT_SECRET, parts, threshold) @pytest.mark.parametrize("secret", [None, bytearray(b"")], ids=["none", "empty"]) def test_split_invalid_secret(secret): with pytest.raises(ValueError, match="Secret must be at least 1 byte long"): split(secret, 5, 3) @pytest.mark.parametrize( "indices", list(itertools.combinations(range(5), 3)), ids=lambda x: "+".join(str(i) for i in x), ) def test_combine_recovers_secret_from_any_threshold_subset(indices): parts = split(COMBINE_SECRET, 5, 3) selected = [parts[i] for i in indices] assert combine(selected) == COMBINE_SECRET @pytest.mark.parametrize("parts", [None, bytearray()], ids=["none", "empty"]) def test_combine_with_too_few_parts(parts): with pytest.raises(ValueError, match="Not enough parts to combine"): combine(parts) def test_combine_with_mismatched_part_lengths(): with pytest.raises(ValueError, match="Parts are not the same length"): combine([bytearray(b"ab"), bytearray(b"abc")]) def test_combine_with_too_short_parts(): with pytest.raises(ValueError, match="Part is too short"): combine([bytearray(b"a"), bytearray(b"b")]) def test_combine_with_duplicate_samples(): with pytest.raises(ValueError, match="Duplicate sample"): combine([bytearray(b"ab"), bytearray(b"ab")]) ================================================ FILE: tests/test_utils.py ================================================ """Direct tests for the GF(256) primitives in pyshamir._utils. These functions are exercised transitively through split/combine in test_shamir.py, but direct tests pin the cryptographic invariants (field axioms, AES Rijndael polynomial reduction, Horner evaluation, Lagrange interpolation) and surface failures closer to their root cause. """ from __future__ import annotations import pytest from pyshamir._utils import ( Polynomial, add, div, generate_x_coordinates, interpolate_polynomial, inverse, make_polynomial, mul, ) # ----- add ----- @pytest.mark.parametrize( "a, b", [(a, b) for a in [0, 1, 0x55, 0xAA, 0xFF] for b in [0, 1, 0x55, 0xAA, 0xFF]] ) def test_add_is_xor(a, b): assert add(a, b) == a ^ b def test_add_zero_is_identity(): for a in range(256): assert add(a, 0) == a assert add(0, a) == a def test_add_self_is_zero(): for a in range(256): assert add(a, a) == 0 # ----- mul ----- def test_mul_zero_annihilates(): for a in range(256): assert mul(a, 0) == 0 assert mul(0, a) == 0 def test_mul_one_is_identity(): for a in range(256): assert mul(a, 1) == a assert mul(1, a) == a @pytest.mark.parametrize( "a, b, expected", [ # mul(2, x) doubles in GF(256); high-bit overflow XORs the Rijndael 0x1B. (2, 1, 2), (2, 0x40, 0x80), (2, 0x80, 0x1B), # 3 = 2 ^ 1, so mul(3, 7) = mul(2, 7) ^ mul(1, 7) = 14 ^ 7 = 9. (3, 7, 9), # FIPS 197 §4.2 worked example: 0x57 * 0x83 = 0xC1. (0x57, 0x83, 0xC1), ], ) def test_mul_known_vectors(a, b, expected): assert mul(a, b) == expected def test_mul_is_commutative(): for a in [0x12, 0x34, 0x56, 0x78]: for b in [0x9A, 0xBC, 0xDE, 0xF0]: assert mul(a, b) == mul(b, a) # ----- inverse ----- def test_inverse_times_self_is_one(): for a in range(1, 256): assert mul(a, inverse(a)) == 1 def test_inverse_of_zero_is_zero(): # 0 has no true multiplicative inverse; the exponentiation chain # collapses to 0, matching the Go reference. assert inverse(0) == 0 # ----- div ----- def test_div_by_zero_raises(): with pytest.raises(ZeroDivisionError, match="Divide by zero"): div(5, 0) def test_div_zero_numerator_returns_zero(): for b in range(1, 256): assert div(0, b) == 0 def test_div_inverts_mul(): for a in [0x01, 0x42, 0xAB, 0xFF]: for b in [0x01, 0x42, 0xAB, 0xFF]: assert div(mul(a, b), b) == a # ----- Polynomial.evaluate ----- def test_polynomial_evaluate_at_zero_returns_intercept(): p = Polynomial(3) p.coefficients[0] = 0x42 p.coefficients[1] = 0x11 p.coefficients[2] = 0x22 p.coefficients[3] = 0x33 assert p.evaluate(0) == 0x42 @pytest.mark.parametrize("x", [1, 2, 0x55, 0xAA, 0xFF]) def test_polynomial_evaluate_matches_manual_horner(x): p = Polynomial(3) p.coefficients[0] = 0x05 p.coefficients[1] = 0x07 p.coefficients[2] = 0x0B p.coefficients[3] = 0x0D # Horner: ((c3 * x + c2) * x + c1) * x + c0, with + as XOR in GF(256). expected = p.coefficients[3] expected = add(mul(expected, x), p.coefficients[2]) expected = add(mul(expected, x), p.coefficients[1]) expected = add(mul(expected, x), p.coefficients[0]) assert p.evaluate(x) == expected # ----- make_polynomial ----- def test_make_polynomial_sets_intercept_and_size(): poly = make_polynomial(0x77, 5) assert poly.coefficients[0] == 0x77 assert len(poly.coefficients) == 6 # degree + 1 # ----- interpolate_polynomial round-trip ----- @pytest.mark.parametrize("intercept", [0x00, 0x01, 0x55, 0xAA, 0xFF]) def test_interpolate_recovers_intercept(intercept): """Build a degree-3 polynomial, sample 4 distinct x's, interpolate at x=0.""" poly = make_polynomial(intercept, 3) xs = bytearray([1, 2, 3, 4]) ys = bytearray([poly.evaluate(x) for x in xs]) assert interpolate_polynomial(xs, ys, 0) == intercept # ----- generate_x_coordinates ----- def test_generate_x_coordinates_returns_permutation(): n = 100 xs = generate_x_coordinates(n) assert len(xs) == n assert sorted(xs) == list(range(n)) def test_generate_x_coordinates_is_shuffled(): # Two independent calls on n=100 collide with probability 1/100! ≈ 0. assert generate_x_coordinates(100) != generate_x_coordinates(100) ================================================ FILE: tests/test_utils_properties.py ================================================ """Hypothesis-based property tests for the GF(256) primitives in pyshamir._utils. These complement the explicit-value tests in test_utils.py by sweeping random inputs across the full byte domain and shrinking failures to minimal counterexamples. Each test pins an algebraic invariant of the field; together they form a partial fuzzing harness recognized by OpenSSF Scorecard's `Fuzzing` check. """ from __future__ import annotations from hypothesis import given, settings from hypothesis import strategies as st from pyshamir._utils import ( add, div, interpolate_polynomial, inverse, make_polynomial, mul, ) byte = st.integers(min_value=0, max_value=255) nonzero_byte = st.integers(min_value=1, max_value=255) small_degree = st.integers(min_value=2, max_value=8) # ----- add (XOR) field axioms ----- @given(a=byte, b=byte) def test_add_is_commutative(a: int, b: int) -> None: assert add(a, b) == add(b, a) @given(a=byte, b=byte, c=byte) def test_add_is_associative(a: int, b: int, c: int) -> None: assert add(add(a, b), c) == add(a, add(b, c)) @given(a=byte) def test_add_zero_is_identity(a: int) -> None: assert add(a, 0) == a # ----- mul field axioms ----- @given(a=byte, b=byte) def test_mul_is_commutative(a: int, b: int) -> None: assert mul(a, b) == mul(b, a) @given(a=byte, b=byte, c=byte) def test_mul_distributes_over_add(a: int, b: int, c: int) -> None: assert mul(a, add(b, c)) == add(mul(a, b), mul(a, c)) @given(a=byte) def test_mul_one_is_identity(a: int) -> None: assert mul(a, 1) == a # ----- inverse / div ----- @given(a=nonzero_byte) def test_inverse_is_multiplicative_inverse(a: int) -> None: assert mul(a, inverse(a)) == 1 @given(a=byte, b=nonzero_byte) def test_div_inverts_mul(a: int, b: int) -> None: assert div(mul(a, b), b) == a # ----- Polynomial / Lagrange round-trip ----- @settings(max_examples=200) @given(intercept=byte, degree=small_degree) def test_interpolate_recovers_intercept(intercept: int, degree: int) -> None: """For any random polynomial of degree d with intercept I, sampling at d+1 distinct non-zero x's and interpolating at x=0 yields I. """ poly = make_polynomial(intercept, degree) xs = bytearray(range(1, degree + 2)) ys = bytearray([poly.evaluate(x) for x in xs]) assert interpolate_polynomial(xs, ys, 0) == intercept ================================================ FILE: tox.ini ================================================ [tox] envlist = py39,py310,py311,py312,py313,lint,type skip_missing_interpreters = true [testenv] deps = pytest pytest-cov hypothesis commands = pytest --cov=pyshamir --cov-report=term-missing [testenv:lint] deps = ruff commands = ruff check pyshamir tests ruff format --check pyshamir tests [testenv:type] deps = mypy commands = mypy pyshamir [testenv:coverage] deps = pytest pytest-cov hypothesis commands = pytest --cov=pyshamir --cov-report=xml --cov-report=html --cov-report=term-missing