├── .git_archival.txt ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── pypi-package.yml │ └── zizmor.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version-default ├── .readthedocs.yaml ├── CHANGELOG.md ├── FAQ.md ├── LICENSE ├── README.md ├── docs ├── Makefile ├── _static │ └── custom.css ├── api.rst ├── argon2.md ├── cli.md ├── conf.py ├── faq.md ├── howto.md ├── index.md ├── installation.md ├── login_example.py └── parameters.md ├── pyproject.toml ├── src └── argon2 │ ├── __init__.py │ ├── __main__.py │ ├── _legacy.py │ ├── _password_hasher.py │ ├── _utils.py │ ├── exceptions.py │ ├── low_level.py │ ├── profiles.py │ └── py.typed ├── tests ├── __init__.py ├── test_legacy.py ├── test_low_level.py ├── test_packaging.py ├── test_password_hasher.py ├── test_utils.py └── typing │ └── api.py ├── tox.ini └── zizmor.yml /.git_archival.txt: -------------------------------------------------------------------------------- 1 | node: $Format:%H$ 2 | node-date: $Format:%cI$ 3 | describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ 4 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socioeconomic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | . 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | First off, thank you for considering contributing! 4 | It's people like *you* who make it is such a great tool for everyone. 5 | 6 | This document is mainly to help you to get started by codifying tribal knowledge and expectations and make it more accessible to everyone. 7 | But don't be afraid to open half-finished PRs and ask questions if something is unclear! 8 | 9 | 10 | ## Workflow 11 | 12 | - No contribution is too small! 13 | Please submit as many fixes for typos and grammar bloopers as you can! 14 | - Try to limit each pull request to *one* change only. 15 | - Since we squash on merge, it's up to you how you handle updates to the main branch. 16 | Whether you prefer to rebase on main or merge main into your branch, do whatever is more comfortable for you. 17 | - *Always* add tests and docs for your code. 18 | This is a hard rule; patches with missing tests or documentation can't be merged. 19 | - Make sure your changes pass our [CI]. 20 | You won't get any feedback until it's green unless you ask for it. 21 | - For the CI to pass, the coverage must be 100%. 22 | If you have problems to test something, open anyway and ask for advice. 23 | In some situations, we may agree to add an `# pragma: no cover`. 24 | - Once you've addressed review feedback, make sure to bump the pull request with a short note, so we know you're done. 25 | - Don’t break backwards-compatibility. 26 | 27 | 28 | ## Local development environment 29 | 30 | First, **fork** the repository on GitHub and **clone** it using one of the alternatives that you can copy-paste by pressing the big green button labeled `<> Code`. 31 | 32 | You can (and should) run our test suite using [*tox*](https://tox.wiki/). 33 | However, you'll probably want a more traditional environment as well. 34 | 35 | We recommend using the Python version from the `.python-version-default` file in the project's root directory, because that's the one that is used in the CI by default, too. 36 | 37 | If you're using [*direnv*](https://direnv.net), you can automate the creation of the project virtual environment with the correct Python version by adding the following `.envrc` to the project root: 38 | 39 | ```bash 40 | layout python python$(cat .python-version-default) 41 | ``` 42 | 43 | or, if you like [*uv*](https://github.com/astral-sh/uv): 44 | 45 | ```bash 46 | test -d .venv || uv venv --python python$(cat .python-version-default) 47 | . .venv/bin/activate 48 | ``` 49 | 50 | > [!WARNING] 51 | > - **Before** you start working on a new pull request, use the "*Sync fork*" button in GitHub's web UI to ensure your fork is up to date. 52 | > - **Always create a new branch off `main` for each new pull request.** 53 | > Yes, you can work on `main` in your fork and submit pull requests. 54 | > But this will *inevitably* lead to you not being able to synchronize your fork with upstream and having to start over. 55 | 56 | Change into the newly created directory and after activating a virtual environment, install an editable version of this project along with its tests requirements: 57 | 58 | ```console 59 | $ pip install -e . --group dev # or `uv pip install -e . --group dev` 60 | ``` 61 | 62 | Now you can run the test suite: 63 | 64 | ```console 65 | $ python -Im pytest 66 | ``` 67 | 68 | When working on the documentation, use: 69 | 70 | ```console 71 | $ tox run -e docs-watch 72 | ``` 73 | 74 | This will build the documentation, and then watch for changes and rebuild it whenever you save a file. 75 | 76 | To just build the documentation and run doctests, use: 77 | 78 | ```console 79 | $ tox run -e docs 80 | ``` 81 | 82 | You will find the built documentation in `docs/_build/html`. 83 | 84 | To avoid committing code that violates our style guide, we strongly advise you to install [*pre-commit*] and its hooks: 85 | 86 | ```console 87 | $ pre-commit install 88 | ``` 89 | 90 | This is not strictly necessary, because our [*tox*] file contains an environment that runs: 91 | 92 | ```console 93 | $ pre-commit run --all-files 94 | ``` 95 | 96 | and our CI has integration with [*pre-commit.ci*](https://pre-commit.ci). 97 | But it's way more comfortable to run it locally and *git* catching avoidable errors. 98 | 99 | 100 | ## Code 101 | 102 | - Obey [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 257](https://www.python.org/dev/peps/pep-0257/). 103 | We use the `"""`-on-separate-lines style for docstrings and [Napoleon](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html) for parsing them: 104 | 105 | ```python 106 | def func(x: str, y: bool) -> int: 107 | """ 108 | Do something. 109 | 110 | Args: 111 | x: A very important parameter. 112 | 113 | y: 114 | Another important parameter whose description is too long for one 115 | line, therefore it starts on the next line. 116 | 117 | Returns: 118 | Something! 119 | """ 120 | ``` 121 | - If you add or change public APIs, tag the docstring using `.. versionadded:: 16.0.0 WHAT` or `.. versionchanged:: 16.2.0 WHAT`. 122 | 123 | - We use [Ruff](https://ruff.rs/) to sort our imports and format our code with a line length of 79 characters. 124 | As long as you run our full [*tox*] suite before committing, or install our [*pre-commit*] hooks (ideally you'll do both – see [*Local Development Environment*](#local-development-environment) above), you won't have to spend any time on formatting your code at all. 125 | If you don't, [CI] will catch it for you – but that seems like a waste of your time! 126 | 127 | 128 | ## Tests 129 | 130 | - Write your asserts as `expected == actual` to line them up nicely: 131 | 132 | ```python 133 | x = f() 134 | 135 | assert 42 == x.some_attribute 136 | assert "foo" == x._a_private_attribute 137 | ``` 138 | 139 | - To run the test suite, all you need is a recent [*tox*]. 140 | It will ensure the test suite runs with all dependencies against all Python versions just as it will in our [CI]. 141 | 142 | - Write [good test docstrings](https://jml.io/pages/test-docstrings.html). 143 | 144 | 145 | ## Documentation 146 | 147 | - Use [semantic newlines] in [*reStructuredText*](https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html) and [Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) files (files ending in `.rst` and `.md`): 148 | 149 | ```rst 150 | This is a sentence. 151 | This is another sentence. 152 | ``` 153 | 154 | 155 | ### Changelog 156 | 157 | If your change is noteworthy, there needs to be a changelog entry in `CHANGELOG.md`. 158 | 159 | - The changelog follows the [*Keep a Changelog*](https://keepachangelog.com/en/1.0.0/) standard. 160 | Please add the best-fitting section if it's missing for the current release. 161 | We use the following order: `Security`, `Removed`, `Deprecated`, `Added`, `Changed`, `Fixed`. 162 | - As with other docs, please use [semantic newlines] in the changelog. 163 | - Make the last line a link to your pull request. 164 | You probably have to open it first to know the number. 165 | - Wrap symbols like modules, functions, or classes into backticks so they are rendered in a `monospace font`. 166 | - Wrap arguments into asterisks like in docstrings: 167 | `Added new argument *an_argument*.` 168 | - If you mention functions or other callables, add parentheses at the end of their names: 169 | `argon2_cffi.func()` or `argon2_cffi.Class.method()`. 170 | This makes the changelog a lot more readable. 171 | - Prefer simple past tense or constructions with "now". 172 | For example: 173 | 174 | * Added `argon2_cffi.func()`. 175 | * `argon2_cffi.func()` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. 176 | 177 | 178 | #### Example entries 179 | 180 | ```markdown 181 | Added `argon2_cffi.func()`. 182 | The feature really *is* awesome. 183 | ``` 184 | 185 | or: 186 | 187 | ```markdown 188 | `argon2_cffi.func()` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. 189 | The bug really *was* nasty. 190 | ``` 191 | 192 | --- 193 | 194 | Again, this list is mainly to help you to get started by codifying tribal knowledge and expectations. 195 | If something is unclear, feel free to ask for help! 196 | 197 | Please note that this project is released with a Contributor [Code of Conduct](https://github.com/hynek/argon2-cffi/blob/main/.github/CODE_OF_CONDUCT.md). 198 | By participating in this project you agree to abide by its terms. 199 | Please report any harm to [Hynek Schlawack] in any way you find appropriate. 200 | 201 | 202 | [CI]: https://github.com/hynek/argon2-cffi/actions 203 | [Hynek Schlawack]: https://hynek.me/about/ 204 | [*pre-commit*]: https://pre-commit.com/ 205 | [*tox*]: https://https://tox.wiki/ 206 | [semantic newlines]: https://rhodesmill.org/brandon/2012/one-sentence-per-line/ 207 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | github: hynek 3 | tidelift: "pypi/argon2_cffi" 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | 4 | 5 | 6 | # Pull Request Check List 7 | 8 | 15 | 16 | - [ ] Do **not** open pull requests from your `main` branch – **use a separate branch**! 17 | - There's a ton of footguns waiting if you don't heed this warning. You can still go back to your project, create a branch from your main branch, push it, and open the pull request from the new branch. 18 | - This is not a pre-requisite for your pull request to be accepted, but **you have been warned**. 19 | - [ ] Added **tests** for changed code. 20 | - The CI fails with less than 100% coverage. 21 | - [ ] **New APIs** are added to our typing tests in [`api.py`](https://github.com/hynek/argon2-cffi/blob/main/tests/typing/api.py). 22 | - [ ] Updated **documentation** for changed code. 23 | - [ ] New functions/classes have to be added to `docs/api.rst` by hand. 24 | - [ ] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded). 25 | - The next version is the second number in the current release + 1. The first number represents the current year. So if the current version on PyPI is 23.1.0, the next version is gonna be 23.2.0. If the next version is the first in the new year, it'll be 24.1.0. 26 | - [ ] Documentation in `.rst` and `.md` files is written using [**semantic newlines**](https://rhodesmill.org/brandon/2012/one-sentence-per-line/). 27 | - [ ] Changes (and possible deprecations) are documented in the [**changelog**](https://github.com/hynek/argon2-cffi/blob/main/CHANGELOG.md). 28 | - [ ] Consider granting [push permissions to the PR branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork), so maintainers can fix minor issues themselves without pestering you. 29 | 30 | 34 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We follow [Calendar Versioning](https://calver.org) with generous backwards-compatibility guarantees. 6 | Therefore, we only support the latest version. 7 | 8 | That said, you shouldn't be afraid to upgrade if you're only using our documented public APIs and pay attention to `DeprecationWarning`s. 9 | Whenever there is a need to break compatibility, it is announced in the changelog and raises a `DeprecationWarning` for a year (if possible) before it's finally really broken. 10 | 11 | > [!WARNING] 12 | > What explicitly *may* change over time are the default [hashing parameters](https://argon2-cffi.readthedocs.io/en/stable/parameters.html) and the behavior of the [CLI interface](https://argon2-cffi.readthedocs.io/en/stable/cli.html). 13 | 14 | 15 | ## Security contact information 16 | 17 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). 18 | Tidelift will coordinate the fix and disclosure. 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: monthly 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | tags: ["*"] 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | env: 12 | FORCE_COLOR: "1" # Make tools pretty. 13 | PIP_DISABLE_PIP_VERSION_CHECK: "1" 14 | PIP_NO_PYTHON_VERSION_WARNING: "1" 15 | 16 | permissions: {} 17 | 18 | 19 | jobs: 20 | build-package: 21 | name: Build & verify package 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | persist-credentials: false 29 | 30 | - uses: hynek/build-and-inspect-python-package@v2 31 | id: baipp 32 | 33 | outputs: 34 | # Used to define the matrix for tests below. The value is based on 35 | # packaging metadata (trove classifiers). 36 | python-versions: ${{ steps.baipp.outputs.supported_python_classifiers_json_array }} 37 | 38 | 39 | tests: 40 | name: Tests & Mypy API on ${{ matrix.python-version }} 41 | runs-on: ubuntu-latest 42 | needs: build-package 43 | 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | # Created by the build-and-inspect-python-package action above. 48 | python-version: ${{ fromJson(needs.build-package.outputs.python-versions) }} 49 | 50 | env: 51 | PYTHON: ${{ matrix.python-version }} 52 | 53 | steps: 54 | - name: Download pre-built packages 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: Packages 58 | path: dist 59 | - run: | 60 | tar xf dist/*.tar.gz --strip-components=1 61 | rm -rf src 62 | - uses: actions/setup-python@v5 63 | with: 64 | python-version: ${{ matrix.python-version }} 65 | allow-prereleases: true 66 | - uses: hynek/setup-cached-uv@v2 67 | 68 | - name: Run tests 69 | run: > 70 | uvx --with tox-uv tox run 71 | --installpkg dist/*.whl 72 | -f py${PYTHON//./}-tests 73 | 74 | - name: Upload coverage data 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: coverage-data-${{ matrix.python-version }} 78 | path: .coverage.* 79 | include-hidden-files: true 80 | if-no-files-found: ignore 81 | 82 | - name: Check public API with Mypy 83 | run: > 84 | uvx --with tox-uv tox run 85 | --installpkg dist/*.whl 86 | -e py${PYTHON//./}-mypy 87 | 88 | 89 | coverage: 90 | name: Ensure 100% test coverage 91 | runs-on: ubuntu-latest 92 | needs: tests 93 | if: always() 94 | 95 | steps: 96 | - uses: actions/checkout@v4 97 | with: 98 | persist-credentials: false 99 | - uses: actions/setup-python@v5 100 | with: 101 | python-version-file: .python-version-default 102 | - uses: hynek/setup-cached-uv@v2 103 | 104 | - name: Download coverage data 105 | uses: actions/download-artifact@v4 106 | with: 107 | pattern: coverage-data-* 108 | merge-multiple: true 109 | 110 | - name: Combine coverage and fail if it's <100%. 111 | run: | 112 | uv tool install coverage 113 | 114 | coverage combine 115 | coverage html --skip-covered --skip-empty 116 | 117 | # Report and write to summary. 118 | coverage report --format=markdown >> $GITHUB_STEP_SUMMARY 119 | 120 | # Report again and fail if under 100%. 121 | coverage report --fail-under=100 122 | 123 | - name: Upload HTML report if check failed. 124 | uses: actions/upload-artifact@v4 125 | with: 126 | name: html-report 127 | path: htmlcov 128 | if: ${{ failure() }} 129 | 130 | 131 | system-package: 132 | name: Install & test with system package of Argon2 133 | runs-on: ubuntu-latest 134 | needs: build-package 135 | 136 | steps: 137 | - name: Download pre-built packages 138 | uses: actions/download-artifact@v4 139 | with: 140 | name: Packages 141 | path: dist 142 | - run: tar xf dist/*.tar.gz --strip-components=1 143 | - uses: actions/setup-python@v5 144 | with: 145 | python-version-file: .python-version-default 146 | 147 | - name: Install dependencies 148 | run: | 149 | sudo apt-get install libargon2-1 libargon2-dev 150 | python -VV 151 | python -Im site 152 | python -Im pip install --upgrade tox 153 | 154 | - run: python -Im tox run -e system-argon2 155 | 156 | 157 | mypy-pkg: 158 | name: Mypy Codebase 159 | runs-on: ubuntu-latest 160 | needs: build-package 161 | 162 | steps: 163 | - name: Download pre-built packages 164 | uses: actions/download-artifact@v4 165 | with: 166 | name: Packages 167 | path: dist 168 | - run: tar xf dist/*.tar.gz --strip-components=1 169 | - uses: actions/setup-python@v5 170 | with: 171 | python-version-file: .python-version-default 172 | - uses: hynek/setup-cached-uv@v2 173 | 174 | - run: > 175 | uvx --with tox-uv 176 | tox run -e mypy-pkg 177 | 178 | pyright: 179 | name: Pyright Codebase 180 | runs-on: ubuntu-latest 181 | needs: build-package 182 | 183 | steps: 184 | - name: Download pre-built packages 185 | uses: actions/download-artifact@v4 186 | with: 187 | name: Packages 188 | path: dist 189 | - run: tar xf dist/*.tar.gz --strip-components=1 190 | - uses: actions/setup-python@v5 191 | with: 192 | python-version-file: .python-version-default 193 | - uses: hynek/setup-cached-uv@v2 194 | 195 | - run: | 196 | uv venv 197 | uv pip install . --group typing 198 | echo "$PWD/.venv/bin" >> $GITHUB_PATH 199 | - uses: jakebailey/pyright-action@b5d50e5cde6547546a5c4ac92e416a8c2c1a1dfe # v2.3.2 200 | 201 | 202 | docs: 203 | name: Run doctests 204 | needs: build-package 205 | runs-on: ubuntu-latest 206 | steps: 207 | - name: Download pre-built packages 208 | uses: actions/download-artifact@v4 209 | with: 210 | name: Packages 211 | path: dist 212 | - run: tar xf dist/*.tar.gz --strip-components=1 213 | - uses: hynek/setup-cached-uv@v2 214 | 215 | - run: > 216 | uvx --with tox-uv 217 | tox run -e docs-doctests 218 | 219 | 220 | install-dev: 221 | name: Verify dev env 222 | runs-on: ${{ matrix.os }} 223 | strategy: 224 | matrix: 225 | os: [ubuntu-latest, windows-latest, macos-latest] 226 | 227 | steps: 228 | - uses: actions/checkout@v4 229 | with: 230 | persist-credentials: false 231 | - uses: actions/setup-python@v5 232 | with: 233 | python-version-file: .python-version-default 234 | 235 | - name: Install in dev mode and run CLI 236 | run: | 237 | python -Im pip install -e . --group dev 238 | python -Im argon2 -n 1 -t 1 -m 8 -p 1 239 | 240 | 241 | required-checks-pass: 242 | if: always() 243 | name: Ensure everything required is passing for branch protection 244 | runs-on: ubuntu-latest 245 | needs: 246 | - coverage 247 | - mypy-pkg 248 | - pyright 249 | - docs 250 | - install-dev 251 | - system-package 252 | 253 | steps: 254 | - name: Decide whether the needed jobs succeeded or failed 255 | uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 256 | with: 257 | jobs: ${{ toJSON(needs) }} 258 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CodeQL 3 | 4 | on: 5 | schedule: 6 | - cron: "30 22 * * 4" 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [python] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | with: 29 | persist-credentials: false 30 | 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v3 33 | with: 34 | languages: ${{ matrix.language }} 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v3 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v3 41 | -------------------------------------------------------------------------------- /.github/workflows/pypi-package.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build & upload PyPI package 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | tags: ["*"] 8 | release: 9 | types: 10 | - published 11 | workflow_dispatch: 12 | 13 | 14 | jobs: 15 | # Always build & lint package. 16 | build-package: 17 | name: Build & verify package 18 | runs-on: ubuntu-latest 19 | permissions: 20 | attestations: write 21 | id-token: write 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | persist-credentials: false 28 | 29 | - uses: hynek/build-and-inspect-python-package@v2 30 | with: 31 | attest-build-provenance-github: 'true' 32 | 33 | 34 | # Upload to Test PyPI on every commit on main. 35 | release-test-pypi: 36 | name: Publish in-dev package to test.pypi.org 37 | environment: release-test-pypi 38 | if: github.repository_owner == 'hynek' && github.event_name == 'push' && github.ref == 'refs/heads/main' 39 | runs-on: ubuntu-latest 40 | needs: build-package 41 | 42 | permissions: 43 | id-token: write 44 | 45 | steps: 46 | - name: Download packages built by build-and-inspect-python-package 47 | uses: actions/download-artifact@v4 48 | with: 49 | name: Packages 50 | path: dist 51 | 52 | - name: Upload package to Test PyPI 53 | uses: pypa/gh-action-pypi-publish@release/v1 54 | with: 55 | repository-url: https://test.pypi.org/legacy/ 56 | 57 | 58 | # Upload to real PyPI on GitHub Releases. 59 | release-pypi: 60 | name: Publish released package to pypi.org 61 | environment: release-pypi 62 | if: github.repository_owner == 'hynek' && github.event.action == 'published' 63 | runs-on: ubuntu-latest 64 | needs: build-package 65 | 66 | permissions: 67 | id-token: write 68 | 69 | steps: 70 | - name: Download packages built by build-and-inspect-python-package 71 | uses: actions/download-artifact@v4 72 | with: 73 | name: Packages 74 | path: dist 75 | 76 | - name: Upload package to PyPI 77 | uses: pypa/gh-action-pypi-publish@release/v1 78 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/woodruffw/zizmor 2 | name: Zizmor 3 | 4 | on: 5 | push: 6 | branches: ["main"] 7 | pull_request: 8 | branches: ["*"] 9 | 10 | permissions: 11 | contents: read 12 | 13 | 14 | jobs: 15 | zizmor: 16 | name: Zizmor latest via PyPI 17 | runs-on: ubuntu-latest 18 | permissions: 19 | security-events: write 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | - uses: hynek/setup-cached-uv@v2 26 | 27 | - name: Run zizmor 🌈 28 | run: uvx zizmor --format sarif . > results.sarif 29 | env: 30 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Upload SARIF file 33 | uses: github/codeql-action/upload-sarif@v3 34 | with: 35 | # Path to SARIF file relative to the root of the repository 36 | sarif_file: results.sarif 37 | # Optional category for the results 38 | # Used to differentiate multiple results for one commit 39 | category: zizmor 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | .cache 4 | .coverage 5 | .coverage.* 6 | .direnv 7 | .envrc 8 | .hypothesis 9 | .mypy_cache 10 | .pytest_cache/ 11 | .tox 12 | .vscode 13 | __pycache__ 14 | dist 15 | docs/_build/ 16 | Justfile 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ci: 3 | autoupdate_schedule: monthly 4 | 5 | repos: 6 | - repo: https://github.com/astral-sh/ruff-pre-commit 7 | rev: v0.11.12 8 | hooks: 9 | - id: ruff-check 10 | args: [--fix, --exit-non-zero-on-fix] 11 | - id: ruff-format 12 | 13 | - repo: https://github.com/econchick/interrogate 14 | rev: 1.7.0 15 | hooks: 16 | - id: interrogate 17 | args: [tests] 18 | 19 | - repo: https://github.com/codespell-project/codespell 20 | rev: v2.4.1 21 | hooks: 22 | - id: codespell 23 | 24 | - repo: https://github.com/pre-commit/pre-commit-hooks 25 | rev: v5.0.0 26 | hooks: 27 | - id: trailing-whitespace 28 | - id: end-of-file-fixer 29 | - id: check-toml 30 | - id: check-yaml 31 | -------------------------------------------------------------------------------- /.python-version-default: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | build: 5 | os: ubuntu-lts-latest 6 | tools: 7 | # Keep version in sync with tox.ini/docs. 8 | python: "3.13" 9 | jobs: 10 | create_environment: 11 | # Need the tags to calculate the version (sometimes). 12 | - git fetch --tags 13 | 14 | - asdf plugin add uv 15 | - asdf install uv latest 16 | - asdf global uv latest 17 | 18 | build: 19 | html: 20 | - uvx --with tox-uv tox run -e docs-build -- $READTHEDOCS_OUTPUT 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Calendar Versioning](https://calver.org/). 6 | 7 | The **first number** of the version is the year. 8 | The **second number** is incremented with each release, starting at 1 for each year. 9 | The **third number** is when we need to start branches for older releases (only for emergencies). 10 | 11 | You can find our backwards-compatibility policy [here](https://github.com/hynek/argon2-cffi/blob/main/.github/SECURITY.md). 12 | 13 | 14 | 15 | 16 | ## [Unreleased](https://github.com/hynek/argon2-cffi/compare/25.1.0...HEAD) 17 | 18 | 19 | ## [25.1.0](https://github.com/hynek/argon2-cffi/compare/23.1.0...25.1.0) - 2025-06-03 20 | 21 | ### Added 22 | 23 | - Official support for Python 3.13 and 3.14. 24 | No code changes were necessary. 25 | 26 | 27 | ### Removed 28 | 29 | - Python 3.7 is not supported anymore. 30 | [#186](https://github.com/hynek/argon2-cffi/pull/186) 31 | 32 | 33 | ### Changed 34 | 35 | - `argon2.PasswordHasher.check_needs_rehash()` now also accepts bytes like the rest of the API. 36 | [#174](https://github.com/hynek/argon2-cffi/pull/174) 37 | 38 | - Improved parameter compatibility handling for Pyodide / WebAssembly environments. 39 | [#190](https://github.com/hynek/argon2-cffi/pull/190) 40 | 41 | 42 | ## [23.1.0](https://github.com/hynek/argon2-cffi/compare/21.3.0...23.1.0) - 2023-08-15 43 | 44 | ### Removed 45 | 46 | - Python 3.6 is not supported anymore. 47 | 48 | 49 | ### Deprecated 50 | 51 | - The `InvalidHash` exception is deprecated in favor of `InvalidHashError`. 52 | No plans for removal currently exist and the names can (but shouldn't) be used interchangeably. 53 | 54 | - `argon2.hash_password()`, `argon2.hash_password_raw()`, and `argon2.verify_password()` that have been soft-deprecated since 2016 are now hard-deprecated. 55 | They now raise `DeprecationWarning`s and will be removed in 2024. 56 | 57 | 58 | ### Added 59 | 60 | - Official support for Python 3.11 and 3.12. 61 | No code changes were necessary. 62 | 63 | - `argon2.exceptions.InvalidHashError` as a replacement for `InvalidHash`. 64 | 65 | - *salt* parameter to `argon2.PasswordHasher.hash()` to allow for custom salts. 66 | This is only useful for specialized use-cases -- leave it on None unless you know exactly what you are doing. 67 | [#153](https://github.com/hynek/argon2-cffi/pull/153) 68 | 69 | 70 | ## [21.3.0](https://github.com/hynek/argon2-cffi/compare/21.2.0...21.3.0) - 2021-12-11 71 | 72 | ### Fixed 73 | 74 | - While the last release added type hints, the fact that it's been missing a `py.typed` file made Mypy ignore them. 75 | [#113](https://github.com/hynek/argon2-cffi/pull/113) 76 | 77 | 78 | ## [21.2.0](https://github.com/hynek/argon2-cffi/compare/21.1.0...21.2.0) - 2021-12-08 79 | 80 | ### Removed 81 | 82 | - Python 3.5 is not supported anymore. 83 | 84 | - The CFFI bindings have been extracted into a separate project: [*argon2-cffi-bindings*] 85 | This makes *argon2-cffi* a Python-only project und should make it easier to contribute to and have more frequent releases with high-level features. 86 | 87 | This change is breaking for users who want to use a system-wide installation of Argon2 instead of our vendored code, because the argument to the ``--no-binary`` argument changed. 88 | Please refer to the [installation guide](https://argon2-cffi.readthedocs.io/en/stable/installation.html). 89 | 90 | 91 | ### Added 92 | 93 | - Thanks to lots of work within [*argon2-cffi-bindings*], there're pre-compiled wheels for many new platforms. 94 | Including: 95 | - Apple Silicon via `universal2` 96 | - Linux on `amd64` and `arm64` 97 | - [*musl libc*](https://musl.libc.org) ([Alpine Linux!](https://www.alpinelinux.org)) on `i686`, `amd64`, and `arm64` 98 | - PyPy 3.8 99 | 100 | We hope to provide wheels for Windows on `arm64` soon, but are waiting for GitHub Actions to support that. 101 | 102 | - `argon2.Parameters.from_parameters()` together with the `argon2.profiles` module that offers easy access to the RFC-recommended configuration parameters and then some. 103 | [#101](https://github.com/hynek/argon2-cffi/pull/101) 104 | [#110](https://github.com/hynek/argon2-cffi/pull/110) 105 | 106 | - The CLI interface now has a `--profile` option that takes any name from `argon2.profiles`. 107 | 108 | - Types! 109 | *argon2-cffi* is now fully typed. 110 | [#112](https://github.com/hynek/argon2-cffi/pull/112) 111 | 112 | 113 | ### Changed 114 | 115 | - `argon2.PasswordHasher` now uses the RFC 9106 low-memory profile by default. 116 | The old defaults are available as `argon2.profiles.PRE_21_2`. 117 | 118 | 119 | ## [21.1.0](https://github.com/hynek/argon2-cffi/compare/20.1.0...21.1.0) - 2021-08-29 120 | 121 | Vendoring Argon2 @ [62358ba](https://github.com/P-H-C/phc-winner-argon2/tree/62358ba2123abd17fccf2a108a301d4b52c01a7c) (20190702) 122 | 123 | ### Removed 124 | 125 | - Microsoft stopped providing the necessary SDKs to ship Python 2.7 wheels and currently the downloads amount to 0.09%. 126 | Therefore we have decided that Python 2.7 is not supported anymore. 127 | 128 | 129 | ### Changed 130 | 131 | - There are indeed no changes whatsoever to the code of *argon2-cffi*. 132 | The Argon2 project also hasn't tagged a new release since July 2019. 133 | There also don't seem to be any important pending fixes. 134 | 135 | This release is mainly about improving the way binary wheels are built (`abi3` on all platforms). 136 | 137 | 138 | ## [20.1.0](https://github.com/hynek/argon2-cffi/compare/19.2.0...20.1.0) - 2020-05-11 139 | 140 | Vendoring Argon2 @ [62358ba](https://github.com/P-H-C/phc-winner-argon2/tree/62358ba2123abd17fccf2a108a301d4b52c01a7c) (20190702) 141 | 142 | 143 | ### Added 144 | 145 | - It is now possible to manually override the detection of SSE2 using the `ARGON2_CFFI_USE_SSE2` environment variable. 146 | 147 | 148 | ## [19.2.0](https://github.com/hynek/argon2-cffi/compare/18.3.0...19.1.0) - 2019-10-27 149 | 150 | Vendoring Argon2 @ [62358ba](https://github.com/P-H-C/phc-winner-argon2/tree/62358ba2123abd17fccf2a108a301d4b52c01a7c) (20190702) 151 | 152 | ### Removed 153 | 154 | - Python 3.4 is not supported anymore. It has been unsupported by the Python core team for a while now and its PyPI downloads are negligible. 155 | 156 | It's very unlikely that *argon2-cffi* will break under 3.4 anytime soon, but we don't test it and don't ship binary wheels for it anymore. 157 | 158 | ### Fixed 159 | 160 | - The dependency on `enum34` is now protected using a PEP 508 marker. 161 | This fixes problems when the sdist is handled by a different interpreter version than the one running it. 162 | [#48](https://github.com/hynek/argon2-cffi/issues/48) 163 | 164 | 165 | ## [19.1.0](https://github.com/hynek/argon2-cffi/compare/18.3.0...19.1.0) - 2019-01-17 166 | 167 | Vendoring Argon2 @ [670229c](https://github.com/P-H-C/phc-winner-argon2/tree/670229c849b9fe882583688b74eb7dfdc846f9f6) (20171227) 168 | 169 | ### Added 170 | 171 | - Added support for Argon2 v1.2 hashes in `argon2.extract_parameters()`. 172 | 173 | 174 | ## [18.3.0](https://github.com/hynek/argon2-cffi/compare/18.2.0...18.3.0) - 2018-08-19 175 | 176 | Vendoring Argon2 @ [670229c](https://github.com/P-H-C/phc-winner-argon2/tree/670229c849b9fe882583688b74eb7dfdc846f9f6) (20171227) 177 | 178 | ### Added 179 | 180 | - `argon2.PasswordHasher`'s hash type is configurable now. 181 | 182 | 183 | ## [18.2.0](https://github.com/hynek/argon2-cffi/compare/18.1.0...18.2.0) - 2018-08-19 184 | 185 | Vendoring Argon2 @ [670229c](https://github.com/P-H-C/phc-winner-argon2/tree/670229c849b9fe882583688b74eb7dfdc846f9f6) (20171227) 186 | 187 | ### Changed 188 | 189 | - The hash type for `argon2.PasswordHasher` is Argon2**id** now. 190 | 191 | This decision has been made based on the recommendations in the latest [Argon2 RFC draft](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-argon2-04#section-4). 192 | [#33](https://github.com/hynek/argon2-cffi/issues/33) 193 | [#34](https://github.com/hynek/argon2-cffi/pull/34) 194 | 195 | - Some of the hash parameters have been made stricter to be closer to said recommendations. 196 | The current goal for a hash verification times is around 50ms. 197 | [#41](https://github.com/hynek/argon2-cffi/pull/41) 198 | 199 | ### Added 200 | 201 | - To make the change of hash type backward compatible, `argon2.PasswordHasher.verify()` now determines the type of the hash and verifies it accordingly. 202 | 203 | - To allow for bespoke decisions about upgrading Argon2 parameters, it's now possible to extract them from a hash via the `argon2.extract_parameters()` function. 204 | [#41](https://github.com/hynek/argon2-cffi/pull/41) 205 | 206 | - Additionally `argon2.PasswordHasher` now has a `check_needs_rehash()` method that allows to verify whether a hash has been created with the instance's parameters or whether it should be rehashed. 207 | [#41](https://github.com/hynek/argon2-cffi/pull/41) 208 | 209 | 210 | ## [18.1.0](https://github.com/hynek/argon2-cffi/compare/16.3.0...18.1.0) - 2018-01-06 211 | 212 | Vendoring Argon2 @ [670229c](https://github.com/P-H-C/phc-winner-argon2/tree/670229c849b9fe882583688b74eb7dfdc846f9f6) (20171227) 213 | 214 | ### Added 215 | 216 | - It is now possible to use the *argon2-cffi* bindings against an Argon2 library that is provided by the system. 217 | 218 | 219 | ## [16.3.0](https://github.com/hynek/argon2-cffi/compare/16.2.0...16.3.0) - 2016-11-10 220 | 221 | Vendoring Argon2 @ [1c4fc41f81f358283755eea88d4ecd05e43b7fd3](https://github.com/P-H-C/phc-winner-argon2/tree/1c4fc41f81f358283755eea88d4ecd05e43b7fd3) (20161029) 222 | 223 | ### Added 224 | 225 | - Add low-level bindings for Argon2id functions. 226 | 227 | ### Fixed 228 | 229 | - Prevent side-effects like the installation of `cffi` if `setup.py` is called with a command that doesn't require it. 230 | [#20](https://github.com/hynek/argon2-cffi/pull/20) 231 | - Fix a bunch of warnings with new `cffi` versions and Python 3.6. 232 | [#14](https://github.com/hynek/argon2-cffi/pull/14) 233 | [#16](https://github.com/hynek/argon2-cffi/issues/16) 234 | 235 | 236 | ## [16.2.0](https://github.com/hynek/argon2-cffi/compare/16.1.0...16.2.0) - 2016-09-10 237 | 238 | Vendoring Argon2 @ [4844d2fee15d44cb19296ddf36029326d17c5aa3](https://github.com/P-H-C/phc-winner-argon2/tree/4844d2fee15d44cb19296ddf36029326d17c5aa3) 239 | 240 | ### Fixed 241 | 242 | - Fixed compilation on Debian 8 (Jessie). 243 | [#13](https://github.com/hynek/argon2-cffi/pull/13) 244 | 245 | 246 | ## [16.1.0](https://github.com/hynek/argon2-cffi/compare/16.0.0...16.1.0) - 2016-04-19 247 | 248 | Vendoring Argon2 @ [00aaa6604501fade85853a4b2f5695611ff6e7c5](https://github.com/P-H-C/phc-winner-argon2/tree/00aaa6604501fade85853a4b2f5695611ff6e7c5). 249 | 250 | ### Added 251 | 252 | - Add `VerifyMismatchError` that is raised if verification fails only because of a password/hash mismatch. 253 | It's a subclass of `VerificationError` therefore this change is completely backwards-compatible. 254 | 255 | ### Changed 256 | 257 | - Add support for [Argon2 1.3](https://mailarchive.ietf.org/arch/msg/cfrg/beOzPh41Hz3cjl5QD7MSRNTi3lA/). 258 | Old hashes remain functional but opportunistic rehashing is strongly recommended. 259 | 260 | ### Removed 261 | 262 | - Python 3.3 and 2.6 aren't supported anymore. 263 | They may work by chance but any support to them has been ceased. 264 | 265 | The last Python 2.6 release was on October 29, 2013 and isn't supported by the CPython core team anymore. 266 | Major Python packages like Django and Twisted dropped Python 2.6 a while ago already. 267 | 268 | Python 3.3 never had a significant user base and wasn't part of any distribution's LTS release. 269 | 270 | 271 | ## [16.0.0](https://github.com/hynek/argon2-cffi/compare/15.0.1...16.0.0) - 2016-01-02 272 | 273 | Vendoring Argon2 @ [421dafd2a8af5cbb215e16da5953663eb101d139](https://github.com/P-H-C/phc-winner-argon2/tree/421dafd2a8af5cbb215e16da5953663eb101d139). 274 | 275 | ### Deprecated 276 | 277 | - `hash_password()`, `hash_password_raw()`, and `verify_password()` should not be used anymore. 278 | For hashing passwords, use the new `argon2.PasswordHasher`. 279 | If you want to implement your own higher-level abstractions, use the new low-level APIs `hash_secret()`, `hash_secret_raw()`, and `verify_secret()` from the `argon2.low_level` module. 280 | If you want to go *really* low-level, `core()` is for you. 281 | The old functions will *not* raise any warnings though and there are *no* immediate plans to remove them. 282 | 283 | ### Added 284 | 285 | - Added `argon2.PasswordHasher`. 286 | A higher-level class specifically for hashing passwords that also works on Unicode strings. 287 | - Added `argon2.low_level` module with low-level API bindings for building own high-level abstractions. 288 | 289 | 290 | ## [15.0.1](https://github.com/hynek/argon2-cffi/compare/15.0.0...15.0.1) - 2015-12-18 291 | 292 | Vendoring Argon2 @ [4fe0d8cda37691228dd5a96a310be57369403a4b](https://github.com/P-H-C/phc-winner-argon2/tree/4fe0d8cda37691228dd5a96a310be57369403a4b). 293 | 294 | ### Fixed 295 | 296 | - Fix `long_description` on PyPI. 297 | 298 | 299 | ## [15.0.0](https://github.com/hynek/argon2-cffi/compare/15.0.0b5...15.0.0) - 2015-12-18 300 | 301 | Vendoring Argon2 @ [4fe0d8cda37691228dd5a96a310be57369403a4b](https://github.com/P-H-C/phc-winner-argon2/tree/4fe0d8cda37691228dd5a96a310be57369403a4b). 302 | 303 | ### Added 304 | 305 | - Conditionally use the [SSE2](https://en.wikipedia.org/wiki/SSE2)-optimized version of `argon2` on x86 architectures. 306 | 307 | ### Changed 308 | 309 | - `verify_password()` doesn't guess the hash type if passed `None` anymore. 310 | Supporting this resulted in measurable overhead (~0.6ms vs 0.8ms on my notebook) since it had to happen in Python. 311 | That means that naïve usage of the API would give attackers an edge. 312 | The new behavior is that it has the same default value as `hash_password()` such that `verify_password(hash_password(b"password"), b"password")` still works. 313 | - Tweaked default parameters to more reasonable values. 314 | Verification should take between 0.5ms and 1ms on recent-ish hardware. 315 | 316 | ### Fixed 317 | 318 | - More packaging fixes. 319 | Most notably compilation on Visual Studio 2010 for Python 3.3 and 3.4. 320 | 321 | 322 | ## [15.0.0b5](https://github.com/hynek/argon2-cffi/tree/15.0.0b5) - 2015-12-10 323 | 324 | Vendoring Argon2 @ [4fe0d8cda37691228dd5a96a310be57369403a4b](https://github.com/P-H-C/phc-winner-argon2/tree/4fe0d8cda37691228dd5a96a310be57369403a4b). 325 | 326 | ### Added 327 | 328 | - Initial work. 329 | Previous betas were only for fixing Windows packaging. 330 | The authors of Argon2 were kind enough to [help me](https://github.com/P-H-C/phc-winner-argon2/issues/44) to get it building under Visual Studio 2008 that we’re forced to use for Python 2.7 on Windows. 331 | 332 | 333 | [*argon2-cffi-bindings*]: https://github.com/hynek/argon2-cffi-bindings 334 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## I'm using *bcrypt* / *PBKDF2* / *scrypt* / *yescrypt*, do I need to migrate? 4 | 5 | Using password hashes that aren't memory hard carries a certain risk but there's **no immediate danger or need for action**. 6 | If however you are deciding how to hash password *today*, Argon2 is the superior, future-proof choice. 7 | 8 | But if you already use one of the hashes mentioned in the question, you should be fine for the foreseeable future. 9 | If you're using *scrypt* or *yescrypt*, you will be probably fine for good. 10 | 11 | 12 | ## Why do the `verify()` methods raise an Exception instead of returning `False`? 13 | 14 | 1. The Argon2 library had no concept of a "wrong password" error in the beginning. 15 | Therefore when writing these bindings, an exception with the full error had to be raised so you could inspect what went actually wrong. 16 | 17 | Changing that now would be a very dangerous break of backwards-compatibility. 18 | 19 | 2. In my opinion, a wrong password should raise an exception such that it can't pass unnoticed by accident. 20 | See also The Zen of Python: "Errors should never pass silently." 21 | 22 | 3. It's more [Pythonic](https://docs.python.org/3/glossary.html#term-EAFP). 23 | 24 | 25 | ## Does *argon2-cffi* release the GIL? 26 | 27 | [Yes](https://cffi.readthedocs.io/en/latest/ref.html#conversions). 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Hynek Schlawack and the argon2-cffi contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # *argon2-cffi*: Argon2 for Python 2 | 3 | [![Documentation](https://img.shields.io/badge/Docs-Read%20The%20Docs-black)](https://argon2-cffi.readthedocs.io/) 4 | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6671/badge)](https://bestpractices.coreinfrastructure.org/projects/6671) 5 | [![PyPI version](https://img.shields.io/pypi/v/argon2-cffi)](https://pypi.org/project/argon2-cffi/) 6 | [![Downloads / Month](https://static.pepy.tech/personalized-badge/argon2-cffi?period=month&units=international_system&left_color=grey&right_color=blue&left_text=Downloads%20/%20Month)](https://pepy.tech/project/argon2-cffi) 7 | 8 | 9 | 10 | 11 | [Argon2](https://github.com/p-h-c/phc-winner-argon2) won the [Password Hashing Competition](https://www.password-hashing.net/) and *argon2-cffi* is the simplest way to use it in Python: 12 | 13 | ```pycon 14 | >>> from argon2 import PasswordHasher 15 | >>> ph = PasswordHasher() 16 | >>> hash = ph.hash("correct horse battery staple") 17 | >>> hash # doctest: +SKIP 18 | '$argon2id$v=19$m=65536,t=3,p=4$MIIRqgvgQbgj220jfp0MPA$YfwJSVjtjSU0zzV/P3S9nnQ/USre2wvJMjfCIjrTQbg' 19 | >>> ph.verify(hash, "correct horse battery staple") 20 | True 21 | >>> ph.check_needs_rehash(hash) 22 | False 23 | >>> ph.verify(hash, "Tr0ub4dor&3") 24 | Traceback (most recent call last): 25 | ... 26 | argon2.exceptions.VerifyMismatchError: The password does not match the supplied hash 27 | 28 | ``` 29 | 30 | 31 | ## Project Links 32 | 33 | - [**PyPI**](https://pypi.org/project/argon2-cffi/) 34 | - [**GitHub**](https://github.com/hynek/argon2-cffi) 35 | - [**Documentation**](https://argon2-cffi.readthedocs.io/) 36 | - [**Changelog**](https://github.com/hynek/argon2-cffi/blob/main/CHANGELOG.md) 37 | - [**Funding**](https://hynek.me/say-thanks/) 38 | - The low-level Argon2 CFFI bindings are maintained in the separate [*argon2-cffi-bindings*](https://github.com/hynek/argon2-cffi-bindings) project. 39 | 40 | 41 | 42 | ## Credits 43 | 44 | *argon2-cffi* is maintained by [Hynek Schlawack](https://hynek.me/). 45 | 46 | The development is kindly supported by my employer [Variomedia AG](https://www.variomedia.de/), *argon2-cffi* [Tidelift subscribers](https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek), and my amazing [GitHub Sponsors](https://github.com/sponsors/hynek). 47 | 48 | 49 | ## *argon2-cffi* for Enterprise 50 | 51 | Available as part of the [Tidelift Subscription](https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek). 52 | 53 | The maintainers of *argon2-cffi* and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open-source packages you use to build your applications. 54 | Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. 55 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/prometheus_async.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/prometheus_async.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/prometheus_async" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/prometheus_async" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | @import url('https://rsms.me/inter/inter.css'); 2 | @import url('https://assets.hynek.me/css/bm.css'); 3 | 4 | 5 | :root { 6 | font-feature-settings: 'liga' 1, 'calt' 1; /* fix for Chrome */ 7 | } 8 | @supports (font-variation-settings: normal) { 9 | :root { font-family: InterVariable, sans-serif; } 10 | } 11 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. module:: argon2 5 | 6 | .. autoclass:: PasswordHasher 7 | :members: from_parameters, hash, verify, check_needs_rehash 8 | 9 | If you don't specify any parameters, the following constants are used: 10 | 11 | .. data:: DEFAULT_RANDOM_SALT_LENGTH 12 | .. data:: DEFAULT_HASH_LENGTH 13 | .. data:: DEFAULT_TIME_COST 14 | .. data:: DEFAULT_MEMORY_COST 15 | .. data:: DEFAULT_PARALLELISM 16 | 17 | They are taken from :data:`argon2.profiles.RFC_9106_LOW_MEMORY`, but they may vary depending on the platform. 18 | You can use :func:`argon2.profiles.get_default_parameters` to get the current platform's defaults. 19 | 20 | 21 | Profiles 22 | -------- 23 | 24 | .. automodule:: argon2.profiles 25 | 26 | 27 | You can try them out using the :doc:`cli` interface. 28 | For example: 29 | 30 | .. code-block:: console 31 | 32 | $ python -m argon2 --profile RFC_9106_HIGH_MEMORY 33 | Running Argon2id 100 times with: 34 | hash_len: 32 bytes 35 | memory_cost: 2097152 KiB 36 | parallelism: 4 threads 37 | time_cost: 1 iterations 38 | 39 | Measuring... 40 | 41 | 866.5ms per password verification 42 | 43 | That should give you a feeling on how they perform in *your* environment. 44 | 45 | .. data:: RFC_9106_HIGH_MEMORY 46 | 47 | Called "FIRST RECOMMENDED option" by `RFC 9106`_. 48 | 49 | Requires beefy 2 GiB, so be careful in memory-contrained systems. 50 | 51 | .. versionadded:: 21.2.0 52 | 53 | .. data:: RFC_9106_LOW_MEMORY 54 | 55 | Called "SECOND RECOMMENDED option" by `RFC 9106`_. 56 | 57 | The main difference is that it only takes 64 MiB of RAM. 58 | 59 | The values from this profile are the default parameters used by :class:`argon2.PasswordHasher`. 60 | 61 | .. versionadded:: 21.2.0 62 | 63 | .. data:: PRE_21_2 64 | 65 | The default values that *argon2-cffi* used from 18.2.0 until 21.2.0. 66 | 67 | Needs 100 MiB of RAM. 68 | 69 | .. versionadded:: 21.2.0 70 | 71 | .. data:: CHEAPEST 72 | 73 | This is the cheapest-possible profile. 74 | 75 | .. warning:: 76 | 77 | This is only for testing purposes! 78 | Do **not** use in production! 79 | 80 | .. versionadded:: 21.2.0 81 | 82 | 83 | .. autofunction:: argon2.profiles.get_default_parameters 84 | 85 | .. _`RFC 9106`: https://www.rfc-editor.org/rfc/rfc9106.html 86 | 87 | 88 | Exceptions 89 | ---------- 90 | 91 | .. autoexception:: argon2.exceptions.VerificationError 92 | 93 | .. autoexception:: argon2.exceptions.VerifyMismatchError 94 | 95 | .. autoexception:: argon2.exceptions.HashingError 96 | 97 | .. autoexception:: argon2.exceptions.InvalidHashError 98 | 99 | .. autoexception:: argon2.exceptions.InvalidHash 100 | 101 | .. autoexception:: argon2.exceptions.UnsupportedParametersError 102 | 103 | 104 | 105 | Utilities 106 | --------- 107 | 108 | .. autofunction:: argon2.extract_parameters 109 | 110 | .. autoclass:: argon2.Parameters 111 | 112 | 113 | Low Level 114 | --------- 115 | 116 | .. automodule:: argon2.low_level 117 | 118 | .. autoclass:: Type() 119 | 120 | .. attribute:: D 121 | 122 | Argon2\ **d** is faster and uses data-depending memory access. 123 | That makes it less suitable for hashing secrets and more suitable for cryptocurrencies and applications with no threats from side-channel timing attacks. 124 | 125 | .. attribute:: I 126 | 127 | Argon2\ **i** uses data-independent memory access. 128 | Argon2i is slower as it makes more passes over the memory to protect from tradeoff attacks. 129 | 130 | .. attribute:: ID 131 | 132 | Argon2\ **id** is a hybrid of Argon2i and Argon2d, using a combination of data-depending and data-independent memory accesses, which gives some of Argon2i's resistance to side-channel cache timing attacks and much of Argon2d's resistance to GPU cracking attacks. 133 | 134 | .. versionadded:: 16.3.0 135 | 136 | .. autodata:: ARGON2_VERSION 137 | 138 | .. autofunction:: hash_secret 139 | 140 | .. doctest:: 141 | 142 | >>> import argon2 143 | >>> argon2.low_level.hash_secret( 144 | ... b"secret", b"somesalt", 145 | ... time_cost=1, memory_cost=8, parallelism=1, hash_len=64, type=argon2.low_level.Type.D 146 | ... ) 147 | b'$argon2d$v=19$m=8,t=1,p=1$c29tZXNhbHQ$ba2qC75j0+JAunZZ/L0hZdQgCv+tOieBuKKXSrQiWm7nlkRcK+YqWr0i0m0WABJKelU8qHJp0SZzH0b1Z+ITvQ' 148 | 149 | 150 | .. autofunction:: verify_secret 151 | 152 | 153 | The raw hash can also be computed: 154 | 155 | .. autofunction:: hash_secret_raw 156 | 157 | .. doctest:: 158 | 159 | >>> argon2.low_level.hash_secret_raw( 160 | ... b"secret", b"somesalt", 161 | ... time_cost=1, memory_cost=8, parallelism=1, hash_len=8, type=argon2.low_level.Type.D 162 | ... ) 163 | b'\xe4n\xf5\xc8|\xa3>\x1d' 164 | 165 | The super low-level ``argon2_core()`` function is exposed too if you need access to very specific options: 166 | 167 | .. autofunction:: core 168 | 169 | In order to use :func:`core`, you need access to *argon2-cffi*'s FFI objects. 170 | Therefore, it is OK to use ``argon2.low_level.ffi`` and ``argon2.low_level.lib`` when working with it. 171 | For example, if you wanted to check the :rfc:`9106` test vectors for Argon2id that include a secret and associated data that both get mixed into the hash and aren't exposed by the high-level APIs: 172 | 173 | .. doctest:: 174 | 175 | >>> from argon2.low_level import Type, core, ffi, lib 176 | 177 | >>> def low_level_hash( 178 | ... password, salt, secret, associated, 179 | ... hash_len, version, t_cost, m_cost, lanes, threads): 180 | ... cout = ffi.new("uint8_t[]", hash_len) 181 | ... cpwd = ffi.new("uint8_t[]", password) 182 | ... cad = ffi.new("uint8_t[]", associated) 183 | ... csalt = ffi.new("uint8_t[]", salt) 184 | ... csecret = ffi.new("uint8_t[]", secret) 185 | ... 186 | ... ctx = ffi.new( 187 | ... "argon2_context *", 188 | ... { 189 | ... "out": cout, 190 | ... "outlen": hash_len, 191 | ... "version": version, 192 | ... "pwd": cpwd, 193 | ... "pwdlen": len(cpwd) - 1, 194 | ... "salt": csalt, 195 | ... "saltlen": len(csalt) - 1, 196 | ... "secret": csecret, 197 | ... "secretlen": len(csecret) - 1, 198 | ... "ad": cad, 199 | ... "adlen": len(cad) - 1, 200 | ... "t_cost": t_cost, 201 | ... "m_cost": m_cost, 202 | ... "lanes": lanes, 203 | ... "threads": threads, 204 | ... "allocate_cbk": ffi.NULL, 205 | ... "free_cbk": ffi.NULL, 206 | ... "flags": lib.ARGON2_DEFAULT_FLAGS, 207 | ... }, 208 | ... ) 209 | ... 210 | ... assert lib.ARGON2_OK == core(ctx, Type.ID.value) 211 | ... 212 | ... return bytes(ffi.buffer(ctx.out, ctx.outlen)).hex() 213 | 214 | >>> password = bytes.fromhex( 215 | ... "0101010101010101010101010101010101010101010101010101010101010101" 216 | ... ) 217 | >>> associated = bytes.fromhex("040404040404040404040404") 218 | >>> salt = bytes.fromhex("02020202020202020202020202020202") 219 | >>> secret = bytes.fromhex("0303030303030303") 220 | 221 | >>> assert ( 222 | ... "0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659" 223 | ... == low_level_hash( 224 | ... password, salt, secret, associated, 225 | ... 32, 19, 3, 32, 4, 4, 226 | ... ) 227 | ... ) 228 | 229 | All constants and types on ``argon2.low_level.lib`` are guaranteed to stay as long they are not altered by Argon2 itself. 230 | 231 | .. autofunction:: error_to_str 232 | 233 | 234 | Deprecated APIs 235 | --------------- 236 | 237 | These APIs are from the first release of *argon2-cffi* and proved to live in an unfortunate mid-level. 238 | On one hand they have defaults and check parameters but on the other hand they only consume byte strings. 239 | 240 | Therefore the decision has been made to replace them by a high-level (:class:`argon2.PasswordHasher`) and a low-level (:mod:`argon2.low_level`) solution. 241 | They will be removed in 2024. 242 | 243 | .. autofunction:: argon2.hash_password 244 | .. autofunction:: argon2.hash_password_raw 245 | .. autofunction:: argon2.verify_password 246 | -------------------------------------------------------------------------------- /docs/argon2.md: -------------------------------------------------------------------------------- 1 | # What is Argon2? 2 | 3 | :::{note} 4 | **TL;DR**: Use {class}`argon2.PasswordHasher` with its default parameters to securely hash your passwords. 5 | 6 | You do **not** need to read or understand anything below this box. 7 | ::: 8 | 9 | Argon2 is a secure password hashing algorithm. 10 | It is designed to have both a configurable runtime as well as memory consumption. 11 | 12 | This means that you can decide how long it takes to hash a password and how much memory is required. 13 | 14 | In September 2021, Argon2 has been standardized by the IETF in {rfc}`9106`. 15 | 16 | Argon2 comes in three variants: Argon2**d**, Argon2**i**, and Argon2**id**. 17 | Argon2**d**'s strength is the resistance against [time–memory trade-offs], while Argon2**i**'s focus is on resistance against [side-channel attacks]. 18 | 19 | Accordingly, Argon2**i** was originally considered the correct choice for password hashing and password-based key derivation. 20 | In practice it turned out that a *combination* of d and i -- that combines their strengths -- is the better choice. 21 | And so Argon2**id** was born and is now considered the *main variant* -- and the only variant required by the RFC to be implemented. 22 | 23 | 24 | ## Why “just use bcrypt” Is Not the Best Answer (Anymore) 25 | 26 | The current workhorses of password hashing are unquestionably [*bcrypt*] and [PBKDF2]. 27 | And while they're still fine to use, the password cracking community embraced new technologies like [GPU]s and [ASIC]s to crack password in a highly parallel fashion. 28 | 29 | An effective measure against extreme parallelism proved making computation of password hashes also *memory* hard. 30 | The best known implementation of that approach is to date [*scrypt*]. 31 | However according to the [Argon2 paper] [^outdated], page 2: 32 | 33 | > \[…\] the existence of a trivial time-memory tradeoff allows compact implementations with the same energy cost. 34 | 35 | Therefore a new algorithm was needed. 36 | This time future-proof and with committee-vetting instead of single implementers. 37 | 38 | [^outdated]: Please note that the paper is in some parts outdated. 39 | For instance it predates the genesis of Argon2**id**. 40 | Generally please refer to {rfc}`9106` instead. 41 | 42 | 43 | ## Password Hashing Competition 44 | 45 | The [Password Hashing Competition] took place between 2012 and 2015 to find a new, secure, and future-proof password hashing algorithm. 46 | Previously the NIST was in charge but after certain events and [revelations] their integrity has been put into question by the general public. 47 | So a group of independent cryptographers and security researchers came together. 48 | 49 | In the end, Argon2 was [announced] as the winner. 50 | 51 | [announced]: https://groups.google.com/forum/#!topic/crypto-competitions/3QNdmwBS98o 52 | [argon2 paper]: https://www.password-hashing.net/argon2-specs.pdf 53 | [asic]: https://en.wikipedia.org/wiki/Application-specific_integrated_circuit 54 | [*bcrypt*]: https://en.wikipedia.org/wiki/Bcrypt 55 | [gpu]: https://hashcat.net/hashcat/ 56 | [password hashing competition]: https://www.password-hashing.net/ 57 | [pbkdf2]: https://en.wikipedia.org/wiki/PBKDF2 58 | [revelations]: https://en.wikipedia.org/wiki/Dual_EC_DRBG 59 | [*scrypt*]: https://en.wikipedia.org/wiki/Scrypt 60 | [side-channel attacks]: https://en.wikipedia.org/wiki/Side-channel_attack 61 | [time–memory trade-offs]: https://en.wikipedia.org/wiki/Space–time_tradeoff 62 | -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | To aid you with finding the parameters, *argon2-cffi* offers a CLI interface that can be accessed using `python -m argon2`. 4 | It will benchmark Argon2's password *verification* in the current environment: 5 | 6 | ```console 7 | $ python -m argon2 8 | Running Argon2id 100 times with: 9 | hash_len: 32 bytes 10 | memory_cost: 65536 KiB 11 | parallelism: 4 threads 12 | time_cost: 3 iterations 13 | 14 | Measuring... 15 | 16 | 45.7ms per password verification 17 | ``` 18 | 19 | You can use command line arguments to set hashing parameters. 20 | Either by setting them one by one (`-t` for time, `-m` for memory, `-p` for parallelism, `-l` for hash length), or by passing `--profile` followed by one of the names from {mod}`argon2.profiles`. 21 | In that case, the other parameters are ignored. 22 | If you don't pass any arguments as above, it runs with {class}`argon2.PasswordHasher`'s default values. 23 | 24 | This should make it much easier to determine the right parameters for your use case and your environment. 25 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | from importlib import metadata 4 | 5 | 6 | # Add any Sphinx extension module names here, as strings. They can be 7 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 8 | # ones. 9 | 10 | extensions = [ 11 | "myst_parser", 12 | "notfound.extension", 13 | "sphinx.ext.autodoc", 14 | "sphinx.ext.doctest", 15 | "sphinx.ext.intersphinx", 16 | "sphinx.ext.todo", 17 | "sphinx.ext.napoleon", 18 | "sphinx_copybutton", 19 | ] 20 | 21 | myst_enable_extensions = ["deflist", "colon_fence"] 22 | 23 | # Add any paths that contain templates here, relative to this directory. 24 | templates_path = ["_templates"] 25 | 26 | # The suffix of source filenames. 27 | source_suffix = ".rst" 28 | 29 | # The master toctree document. 30 | master_doc = "index" 31 | 32 | # General information about the project. 33 | project = "argon2-cffi" 34 | copyright = "2015, Hynek Schlawack" 35 | 36 | # The version info for the project you're documenting, acts as replacement for 37 | # |version| and |release|, also used in various other places throughout the 38 | # built documents. 39 | # 40 | # The full version, including alpha/beta/rc tags. 41 | if "dev" in (release := metadata.version("argon2-cffi")): 42 | release = version = "UNRELEASED" 43 | else: 44 | # The short X.Y version. 45 | version = release.rsplit(".", 1)[0] 46 | 47 | # Move type hints into the description block, instead of the func definition. 48 | autodoc_typehints = "description" 49 | autodoc_typehints_description_target = "documented" 50 | 51 | 52 | # List of patterns, relative to source directory, that match files and 53 | # directories to ignore when looking for source files. 54 | exclude_patterns = ["_build"] 55 | 56 | # nitpick_ignore = [] 57 | 58 | # The reST default role (used for this markup: `text`) to use for all 59 | # documents. 60 | default_role = "any" 61 | 62 | # If true, '()' will be appended to :func: etc. cross-reference text. 63 | add_function_parentheses = True 64 | 65 | 66 | # -- Options for HTML output ---------------------------------------------- 67 | 68 | html_theme = "furo" 69 | html_theme_options = { 70 | "top_of_page_buttons": [], 71 | "light_css_variables": { 72 | "font-stack": "Inter,sans-serif", 73 | "font-stack--monospace": "BerkeleyMono, MonoLisa, ui-monospace, " 74 | "SFMono-Regular, Menlo, Consolas, Liberation Mono, monospace", 75 | }, 76 | } 77 | html_static_path = ["_static"] 78 | html_css_files = ["custom.css"] 79 | 80 | # Output file base name for HTML help builder. 81 | htmlhelp_basename = "argon2-cffidoc" 82 | 83 | 84 | # Grouping the document tree into LaTeX files. List of tuples 85 | # (source start file, target name, title, 86 | # author, documentclass [howto, manual, or own class]). 87 | latex_documents = [ 88 | ( 89 | "index", 90 | "argon2-cffi.tex", 91 | "argon2-cffi Documentation", 92 | "Hynek Schlawack", 93 | "manual", 94 | ) 95 | ] 96 | 97 | # -- Options for manual page output --------------------------------------- 98 | 99 | # One entry per manual page. List of tuples 100 | # (source start file, name, description, authors, manual section). 101 | man_pages = [ 102 | ( 103 | "index", 104 | "argon2-cffi", 105 | "argon2-cffi Documentation", 106 | ["Hynek Schlawack"], 107 | 1, 108 | ) 109 | ] 110 | 111 | 112 | # -- Options for Texinfo output ------------------------------------------- 113 | 114 | # Grouping the document tree into Texinfo files. List of tuples 115 | # (source start file, target name, title, author, 116 | # dir menu entry, description, category) 117 | texinfo_documents = [ 118 | ( 119 | "index", 120 | "argon2-cffi", 121 | "argon2-cffi Documentation", 122 | "Hynek Schlawack", 123 | "argon2-cffi", 124 | "Argon2 for Python", 125 | "Miscellaneous", 126 | ) 127 | ] 128 | 129 | 130 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 131 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | ```{include} ../FAQ.md 2 | ``` 3 | -------------------------------------------------------------------------------- /docs/howto.md: -------------------------------------------------------------------------------- 1 | # How to Hash a Password 2 | 3 | *argon2-cffi* comes with an high-level API and uses the officially recommended low-memory Argon2 parameters that result in a verification time of 40--50ms on recent-ish hardware. 4 | 5 | :::{warning} 6 | The current memory requirement is set to rather conservative 64 MB. 7 | However, in memory constrained environments such as Docker containers that can lead to problems. 8 | One possible non-obvious symptom are apparent freezes that are caused by swapping. 9 | 10 | Please check {doc}`parameters` for more details. 11 | ::: 12 | 13 | Unless you have any special requirements, all you need to know is: 14 | 15 | ```python 16 | >>> from argon2 import PasswordHasher 17 | >>> ph = PasswordHasher() 18 | >>> hash = ph.hash("correct horse battery staple") 19 | >>> hash # doctest: +SKIP 20 | '$argon2id$v=19$m=65536,t=3,p=4$MIIRqgvgQbgj220jfp0MPA$YfwJSVjtjSU0zzV/P3S9nnQ/USre2wvJMjfCIjrTQbg' 21 | >>> ph.verify(hash, "correct horse battery staple") 22 | True 23 | >>> ph.check_needs_rehash(hash) 24 | False 25 | >>> ph.verify(hash, "Tr0ub4dor&3") 26 | Traceback (most recent call last): 27 | ... 28 | argon2.exceptions.VerifyMismatchError: The password does not match the supplied hash 29 | ``` 30 | 31 | A login function could thus look like this: 32 | 33 | ```{literalinclude} login_example.py 34 | ``` 35 | 36 | --- 37 | 38 | While the {class}`argon2.PasswordHasher` class has the aspiration to be good to use out of the box, it has all the parametrization you'll need. 39 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # *argon2-cffi*: Argon2 for Python 2 | 3 | Release **{sub-ref}`release`** ([What's new?](https://github.com/hynek/argon2-cffi/blob/main/CHANGELOG.md)) 4 | 5 | ```{include} ../README.md 6 | :end-before: 7 | :start-after: 8 | ``` 9 | 10 | If you don't know where to start, learn {doc}`argon2` and take it from there! 11 | 12 | 13 | ## Indices and Tables 14 | 15 | - {doc}`api` 16 | - {ref}`genindex` 17 | - {ref}`search` 18 | 19 | 20 | ```{toctree} 21 | :hidden: 22 | :maxdepth: 1 23 | 24 | argon2 25 | installation 26 | howto 27 | api 28 | parameters 29 | cli 30 | faq 31 | ``` 32 | 33 | 34 | ```{toctree} 35 | :hidden: 36 | :caption: Meta 37 | 38 | PyPI 39 | GitHub 40 | Changelog 41 | Contributing 42 | Security Policy 43 | Funding 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Using a Vendored Argon2 4 | 5 | ```console 6 | $ python -Im pip install argon2-cffi 7 | ``` 8 | 9 | should be all it takes. 10 | 11 | But since *argon2-cffi* depends on [argon2-cffi-bindings] that vendors Argon2's C code by default, it can lead to complications depending on the platform. 12 | 13 | The C code is known to compile and work on all common platforms (including x86, ARM, and PPC). 14 | On x86, an [SSE2]-optimized version is used. 15 | 16 | If something goes wrong, please try to update your *pip* package first: 17 | 18 | ```console 19 | $ python -Im pip install -U pip 20 | ``` 21 | 22 | Overall this should be the safest bet because *argon2-cffi* has been specifically tested against the vendored version. 23 | 24 | 25 | ### Wheels 26 | 27 | Binary [wheels](https://pythonwheels.com) for macOS, Windows, and Linux are provided on [PyPI] by [argon2-cffi-bindings]. 28 | With a recent-enough *pip* they should be used automatically. 29 | 30 | 31 | ### Source Distribution 32 | 33 | A working C compiler and [CFFI environment] are required to build the [argon2-cffi-bindings] dependency. 34 | If you've been able to compile Python CFFI extensions before, *argon2-cffi* should install without any problems. 35 | 36 | 37 | ## Using a System-wide Installation of Argon2 38 | 39 | If you set `ARGON2_CFFI_USE_SYSTEM` to `1` (and *only* `1`), *argon2-cffi-bindings* will not build its bindings. 40 | However binary wheels are preferred by *pip* and Argon2 gets installed along with *argon2-cffi* anyway. 41 | 42 | Therefore you also have to instruct *pip* to use a source distribution of [argon2-cffi-bindings]: 43 | 44 | ```console 45 | $ env ARGON2_CFFI_USE_SYSTEM=1 \ 46 | python -m pip install --no-binary=argon2-cffi-bindings argon2-cffi 47 | ``` 48 | 49 | This approach can lead to problems around your build chain and you can run into incompatibilities between Argon2 and *argon2-cffi* if the latter has been tested against a different version. 50 | 51 | **It is your own responsibility to deal with these risks if you choose this path.** 52 | 53 | :::{versionadded} 18.1.0 54 | ::: 55 | 56 | :::{versionchanged} 21.2.0 57 | The `--no-binary` option value changed due to the outsourcing of the binary bindings. 58 | ::: 59 | 60 | 61 | ## Override Automatic SSE2 Detection 62 | 63 | Usually the build process tries to guess whether or not it should use [SSE2]-optimized code. 64 | Despite our best efforts, this can go wrong. 65 | 66 | Therefore you can use the `ARGON2_CFFI_USE_SSE2` environment variable to control the process: 67 | 68 | - If you set it to `1`, *argon2-cffi* will build **with** SSE2 support. 69 | - If you set it to `0`, *argon2-cffi* will build **without** SSE2 support. 70 | - If you set it to anything else, it will be ignored and *argon2-cffi* will try to guess. 71 | 72 | :::{versionadded} 20.1.0 73 | ::: 74 | 75 | [argon2-cffi-bindings]: https://github.com/hynek/argon2-cffi-bindings 76 | [cffi environment]: https://cffi.readthedocs.io/en/latest/installation.html 77 | [pypi]: https://pypi.org/project/argon2-cffi-bindings/ 78 | [sse2]: https://en.wikipedia.org/wiki/SSE2 79 | -------------------------------------------------------------------------------- /docs/login_example.py: -------------------------------------------------------------------------------- 1 | import argon2 2 | 3 | 4 | ph = argon2.PasswordHasher() 5 | 6 | 7 | def login(db, user, password): 8 | hash = db.get_password_hash_for_user(user) 9 | 10 | # Verify password, raises exception if wrong. 11 | ph.verify(hash, password) 12 | 13 | # Now that we have the cleartext password, 14 | # check the hash's parameters and if outdated, 15 | # rehash the user's password in the database. 16 | if ph.check_needs_rehash(hash): 17 | db.set_password_hash_for_user(user, ph.hash(password)) 18 | -------------------------------------------------------------------------------- /docs/parameters.md: -------------------------------------------------------------------------------- 1 | # Choosing Parameters 2 | 3 | :::{note} 4 | You can probably just use {class}`argon2.PasswordHasher` with its default values and be fine. 5 | But it's good to double check using *argon2-cffi*'s {doc}`cli` client, whether its defaults are too slow or too fast for your use case. 6 | ::: 7 | 8 | Finding the right parameters for a password hashing algorithm is a daunting task. 9 | As of September 2021, we have the official Internet standard [RFC 9106] to help us with it. 10 | 11 | It comes with two recommendations in [section 4](https://www.rfc-editor.org/rfc/rfc9106.html#section-4), that (as of *argon2-cffi* 21.2.0) you can load directly from the {mod}`argon2.profiles` module: {data}`argon2.profiles.RFC_9106_HIGH_MEMORY` (called "FIRST RECOMMENDED") and {data}`argon2.profiles.RFC_9106_LOW_MEMORY` ("SECOND RECOMMENDED") into {meth}`argon2.PasswordHasher.from_parameters()`. 12 | 13 | Please use the {doc}`cli` interface together with its `--profile` argument to see if they work for you. 14 | 15 | --- 16 | 17 | If you need finer tuning, the current recommended best practice is as follow: 18 | 19 | 1. Choose whether you want Argon2i, Argon2d, or Argon2id (`type`). 20 | If you don't know what that means, choose Argon2id ({attr}`argon2.low_level.Type.ID`). 21 | 22 | 2. Figure out how many threads can be used on each call to Argon2 (`parallelism`, called "lanes" in the RFC). 23 | They recommend 4 threads. 24 | 25 | 3. Figure out how much memory each call can afford (`memory_cost`). 26 | The APIs use [Kibibytes] (1024 bytes) as base unit. 27 | 28 | 4. Select the salt length. 29 | 16 bytes is sufficient for all applications, but can be reduced to 8 bytes in the case of space constraints. 30 | 31 | 5. Choose a hash length (`hash_len`, called "tag length" in the documentation). 32 | 16 bytes is sufficient for password verification. 33 | 34 | 6. Figure out how long each call can take. 35 | One [recommendation](https://web.archive.org/web/20160304024620/https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2015/march/enough-with-the-salts-updates-on-secure-password-schemes/) for concurrent user logins is to keep it under 0.5 ms. 36 | The RFC used to recommend under 500 ms. 37 | The truth is somewhere between those two values: more is more secure, less is a better user experience. 38 | *argon2-cffi*'s current defaults land with ~50ms somewhere in the middle, but the actual time depends on your hardware. 39 | 40 | Please note though, that even a verification time of 1 second won't protect you against bad passwords from the "top 10,000 passwords" lists that you can find online. 41 | 42 | 7. Measure the time for hashing using your chosen parameters. 43 | Start with `time_cost=1` and measure the time it takes. 44 | Raise `time_cost` until it is within your accounted time. 45 | If `time_cost=1` takes too long, lower `memory_cost`. 46 | 47 | *argon2-cffi*'s {doc}`cli` will help you with this process. 48 | 49 | :::{note} 50 | Alternatively, you can also refer to the [OWASP cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id). 51 | ::: 52 | 53 | [kibibytes]: https://en.wikipedia.org/wiki/Kibibyte 54 | [rfc 9106]: https://www.rfc-editor.org/rfc/rfc9106.html 55 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | [build-system] 4 | requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme"] 5 | build-backend = "hatchling.build" 6 | 7 | 8 | [tool.hatch.build.targets.wheel] 9 | packages = ["src/argon2"] 10 | 11 | 12 | [project] 13 | name = "argon2-cffi" 14 | description = "Argon2 for Python" 15 | authors = [{ name = "Hynek Schlawack", email = "hs@ox.cx" }] 16 | dynamic = ["version", "readme"] 17 | requires-python = ">=3.8" 18 | license = "MIT" 19 | license-files = ["LICENSE"] 20 | keywords = ["password", "hash", "hashing", "security"] 21 | classifiers = [ 22 | "Development Status :: 5 - Production/Stable", 23 | "Operating System :: MacOS :: MacOS X", 24 | "Operating System :: Microsoft :: Windows", 25 | "Operating System :: POSIX", 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Programming Language :: Python :: 3.11", 30 | "Programming Language :: Python :: 3.12", 31 | "Programming Language :: Python :: 3.13", 32 | "Programming Language :: Python :: 3.14", 33 | "Programming Language :: Python :: Implementation :: CPython", 34 | "Programming Language :: Python :: Implementation :: PyPy", 35 | "Topic :: Security :: Cryptography", 36 | "Typing :: Typed", 37 | ] 38 | dependencies = ["argon2-cffi-bindings"] 39 | 40 | [dependency-groups] 41 | tests = ["hypothesis", "pytest"] 42 | typing = ["mypy"] 43 | docs = [ 44 | "sphinx", 45 | "sphinx-notfound-page", 46 | "sphinx-copybutton", 47 | "furo", 48 | "myst-parser", 49 | ] 50 | dev = [{ include-group = "tests" }, { include-group = "typing" }, "tox>4"] 51 | 52 | [project.urls] 53 | Documentation = "https://argon2-cffi.readthedocs.io/" 54 | Changelog = "https://github.com/hynek/argon2-cffi/blob/main/CHANGELOG.md" 55 | GitHub = "https://github.com/hynek/argon2-cffi" 56 | Funding = "https://github.com/sponsors/hynek" 57 | Tidelift = "https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek" 58 | 59 | 60 | [tool.hatch.version] 61 | source = "vcs" 62 | raw-options = { local_scheme = "no-local-version" } 63 | 64 | 65 | [tool.hatch.metadata.hooks.fancy-pypi-readme] 66 | content-type = "text/markdown" 67 | 68 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 69 | text = "# *argon2-cffi*: Argon2 for Python\n\n" 70 | 71 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 72 | path = "README.md" 73 | start-after = "\n" 74 | end-before = "\n" 75 | 76 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 77 | text = """ 78 | 79 | ## Release Information 80 | 81 | """ 82 | 83 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 84 | path = "CHANGELOG.md" 85 | start-after = "" 86 | pattern = "\n(###.+?\n)## " 87 | 88 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 89 | text = """ 90 | --- 91 | 92 | [Full Changelog →](https://github.com/hynek/argon2-cffi/blob/main/CHANGELOG.md) 93 | 94 | 95 | """ 96 | 97 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 98 | path = "README.md" 99 | start-at = "## Credits" 100 | 101 | 102 | [tool.pytest.ini_options] 103 | addopts = ["-ra", "--strict-markers", "--strict-config"] 104 | xfail_strict = true 105 | testpaths = "tests" 106 | filterwarnings = ["once::Warning"] 107 | 108 | 109 | [tool.coverage.run] 110 | parallel = true 111 | branch = true 112 | source = ["argon2"] 113 | 114 | [tool.coverage.paths] 115 | source = ["src", ".tox/py*/**/site-packages"] 116 | 117 | [tool.coverage.report] 118 | show_missing = true 119 | skip_covered = true 120 | exclude_lines = [ 121 | # a more strict default pragma 122 | "\\# pragma: no cover\\b", 123 | 124 | # allow defensive code 125 | "^\\s*raise AssertionError\\b", 126 | "^\\s*raise NotImplementedError\\b", 127 | "^\\s*return NotImplemented\\b", 128 | "^\\s*raise$", 129 | 130 | # typing-related code 131 | "^if (False|TYPE_CHECKING):", 132 | ": \\.\\.\\.(\\s*#.*)?$", 133 | "^ +\\.\\.\\.$", 134 | "-> ['\"]?NoReturn['\"]?:", 135 | ] 136 | omit = [] 137 | 138 | 139 | [tool.interrogate] 140 | verbose = 2 141 | fail-under = 100 142 | whitelist-regex = ["test_.*"] 143 | 144 | 145 | [tool.pyright] 146 | ignore = ["conftest.py", "docs", "tests"] 147 | disableBytesTypePromotions = true 148 | 149 | 150 | [tool.mypy] 151 | strict = true 152 | pretty = true 153 | 154 | show_error_codes = true 155 | enable_error_code = ["ignore-without-code"] 156 | 157 | ignore_missing_imports = true 158 | 159 | 160 | [[tool.mypy.overrides]] 161 | module = "tests.*" 162 | ignore_errors = true 163 | 164 | 165 | [tool.ruff] 166 | src = ["src", "tests", "noxfile.py"] 167 | line-length = 79 168 | 169 | [tool.ruff.lint] 170 | select = ["ALL"] 171 | ignore = [ 172 | "A001", # shadowing is fine 173 | "A002", # shadowing is fine 174 | "A003", # shadowing is fine 175 | "ANN", # Mypy is better at this 176 | "ARG001", # unused arguments are normal when implementing interfaces 177 | "COM", # Formatter takes care of our commas 178 | "D", # We prefer our own docstring style. 179 | "E501", # leave line-length enforcement to formatter 180 | "ERA001", # Dead code detection is overly eager. 181 | "FBT", # we have one function that takes one bool; c'mon! 182 | "FIX", # Yes, we want XXX as a marker. 183 | "INP001", # sometimes we want Python files outside of packages 184 | "ISC001", # conflicts with ruff format 185 | "PLR0913", # yes, many arguments, but most have defaults 186 | "PLR2004", # numbers are sometimes fine 187 | "PLW2901", # re-assigning within loop bodies is fine 188 | "RUF001", # leave my smart characters alone 189 | "SLF001", # private members are accessed by friendly functions 190 | "TCH", # TYPE_CHECKING blocks break autodocs 191 | "TD", # we don't follow other people's todo style 192 | ] 193 | 194 | [tool.ruff.lint.per-file-ignores] 195 | "src/argon2/__main__.py" = ["T201"] # need print in CLI 196 | "tests/*" = [ 197 | "ARG", # stubs don't care about arguments 198 | "S101", # assert 199 | "SIM300", # Yoda rocks in asserts 200 | "PT005", # we always add underscores and explicit name 201 | "PT011", # broad is fine 202 | "TRY002", # stock exceptions are fine in tests 203 | "EM101", # no need for exception msg hygiene in tests 204 | ] 205 | 206 | 207 | [tool.ruff.lint.isort] 208 | lines-between-types = 1 209 | lines-after-imports = 2 210 | -------------------------------------------------------------------------------- /src/argon2/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | """ 4 | Argon2 for Python 5 | """ 6 | 7 | from . import exceptions, low_level, profiles 8 | from ._legacy import hash_password, hash_password_raw, verify_password 9 | from ._password_hasher import ( 10 | DEFAULT_HASH_LENGTH, 11 | DEFAULT_MEMORY_COST, 12 | DEFAULT_PARALLELISM, 13 | DEFAULT_RANDOM_SALT_LENGTH, 14 | DEFAULT_TIME_COST, 15 | PasswordHasher, 16 | ) 17 | from ._utils import Parameters, extract_parameters 18 | from .low_level import Type 19 | 20 | 21 | __title__ = "argon2-cffi" 22 | 23 | __author__ = "Hynek Schlawack" 24 | __copyright__ = "Copyright (c) 2015 " + __author__ 25 | __license__ = "MIT" 26 | 27 | 28 | __all__ = [ 29 | "DEFAULT_HASH_LENGTH", 30 | "DEFAULT_MEMORY_COST", 31 | "DEFAULT_PARALLELISM", 32 | "DEFAULT_RANDOM_SALT_LENGTH", 33 | "DEFAULT_TIME_COST", 34 | "Parameters", 35 | "PasswordHasher", 36 | "Type", 37 | "exceptions", 38 | "extract_parameters", 39 | "hash_password", 40 | "hash_password_raw", 41 | "low_level", 42 | "profiles", 43 | "verify_password", 44 | ] 45 | 46 | 47 | def __getattr__(name: str) -> str: 48 | dunder_to_metadata = { 49 | "__version__": "version", 50 | "__description__": "summary", 51 | "__uri__": "", 52 | "__url__": "", 53 | "__email__": "", 54 | } 55 | if name not in dunder_to_metadata: 56 | msg = f"module {__name__} has no attribute {name}" 57 | raise AttributeError(msg) 58 | 59 | import warnings 60 | 61 | from importlib.metadata import metadata 62 | 63 | warnings.warn( 64 | f"Accessing argon2.{name} is deprecated and will be " 65 | "removed in a future release. Use importlib.metadata directly " 66 | "to query for argon2-cffi's packaging metadata.", 67 | DeprecationWarning, 68 | stacklevel=2, 69 | ) 70 | 71 | meta = metadata("argon2-cffi") 72 | 73 | if name in ("__uri__", "__url__"): 74 | return meta["Project-URL"].split(" ", 1)[-1] 75 | 76 | if name == "__email__": 77 | return meta["Author-email"].split("<", 1)[1].rstrip(">") 78 | 79 | return meta[dunder_to_metadata[name]] 80 | -------------------------------------------------------------------------------- /src/argon2/__main__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | from __future__ import annotations 4 | 5 | import argparse 6 | import sys 7 | import timeit 8 | 9 | from . import ( 10 | DEFAULT_HASH_LENGTH, 11 | DEFAULT_MEMORY_COST, 12 | DEFAULT_PARALLELISM, 13 | DEFAULT_TIME_COST, 14 | PasswordHasher, 15 | profiles, 16 | ) 17 | 18 | 19 | def main(argv: list[str]) -> None: 20 | parser = argparse.ArgumentParser( 21 | description="Benchmark Argon2.", 22 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 23 | ) 24 | parser.add_argument( 25 | "-n", type=int, default=100, help="Number of iterations to measure." 26 | ) 27 | parser.add_argument( 28 | "-t", type=int, help="`time_cost`", default=DEFAULT_TIME_COST 29 | ) 30 | parser.add_argument( 31 | "-m", type=int, help="`memory_cost`", default=DEFAULT_MEMORY_COST 32 | ) 33 | parser.add_argument( 34 | "-p", type=int, help="`parallelism`", default=DEFAULT_PARALLELISM 35 | ) 36 | parser.add_argument( 37 | "-l", type=int, help="`hash_length`", default=DEFAULT_HASH_LENGTH 38 | ) 39 | parser.add_argument( 40 | "--profile", 41 | type=str, 42 | help="A profile from `argon2.profiles. Takes precedence.", 43 | default=None, 44 | ) 45 | 46 | args = parser.parse_args(argv[1:]) 47 | 48 | password = b"secret" 49 | if args.profile: 50 | ph = PasswordHasher.from_parameters( 51 | getattr(profiles, args.profile.upper()) 52 | ) 53 | else: 54 | ph = PasswordHasher( 55 | time_cost=args.t, 56 | memory_cost=args.m, 57 | parallelism=args.p, 58 | hash_len=args.l, 59 | ) 60 | hash = ph.hash(password) 61 | 62 | print(f"Running Argon2id {args.n} times with:") 63 | 64 | for name, value, units in [ 65 | ("hash_len", ph.hash_len, "bytes"), 66 | ("memory_cost", ph.memory_cost, "KiB"), 67 | ("parallelism", ph.parallelism, "threads"), 68 | ("time_cost", ph.time_cost, "iterations"), 69 | ]: 70 | print(f"{name}: {value} {units}") 71 | 72 | print("\nMeasuring...") 73 | duration = timeit.timeit( 74 | f"ph.verify({hash!r}, {password!r})", 75 | setup=f"""\ 76 | from argon2 import PasswordHasher 77 | 78 | ph = PasswordHasher( 79 | time_cost={args.t!r}, 80 | memory_cost={args.m!r}, 81 | parallelism={args.p!r}, 82 | hash_len={args.l!r}, 83 | ) 84 | gc.enable()""", 85 | number=args.n, 86 | ) 87 | print(f"\n{duration / args.n * 1000:.1f}ms per password verification") 88 | 89 | 90 | if __name__ == "__main__": # pragma: no cover 91 | main(sys.argv) 92 | -------------------------------------------------------------------------------- /src/argon2/_legacy.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | """ 4 | Legacy mid-level functions. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import os 10 | import warnings 11 | 12 | from typing import Literal 13 | 14 | from ._password_hasher import ( 15 | DEFAULT_HASH_LENGTH, 16 | DEFAULT_MEMORY_COST, 17 | DEFAULT_PARALLELISM, 18 | DEFAULT_RANDOM_SALT_LENGTH, 19 | DEFAULT_TIME_COST, 20 | ) 21 | from .low_level import Type, hash_secret, hash_secret_raw, verify_secret 22 | 23 | 24 | _INSTEAD = " is deprecated, use argon2.PasswordHasher instead" 25 | 26 | 27 | def hash_password( 28 | password: bytes, 29 | salt: bytes | None = None, 30 | time_cost: int = DEFAULT_TIME_COST, 31 | memory_cost: int = DEFAULT_MEMORY_COST, 32 | parallelism: int = DEFAULT_PARALLELISM, 33 | hash_len: int = DEFAULT_HASH_LENGTH, 34 | type: Type = Type.I, 35 | ) -> bytes: 36 | """ 37 | Legacy alias for :func:`argon2.low_level.hash_secret` with default 38 | parameters. 39 | 40 | .. deprecated:: 16.0.0 41 | Use :class:`argon2.PasswordHasher` for passwords. 42 | """ 43 | warnings.warn( 44 | "argon2.hash_password" + _INSTEAD, DeprecationWarning, stacklevel=2 45 | ) 46 | if salt is None: 47 | salt = os.urandom(DEFAULT_RANDOM_SALT_LENGTH) 48 | return hash_secret( 49 | password, salt, time_cost, memory_cost, parallelism, hash_len, type 50 | ) 51 | 52 | 53 | def hash_password_raw( 54 | password: bytes, 55 | salt: bytes | None = None, 56 | time_cost: int = DEFAULT_TIME_COST, 57 | memory_cost: int = DEFAULT_MEMORY_COST, 58 | parallelism: int = DEFAULT_PARALLELISM, 59 | hash_len: int = DEFAULT_HASH_LENGTH, 60 | type: Type = Type.I, 61 | ) -> bytes: 62 | """ 63 | Legacy alias for :func:`argon2.low_level.hash_secret_raw` with default 64 | parameters. 65 | 66 | .. deprecated:: 16.0.0 67 | Use :class:`argon2.PasswordHasher` for passwords. 68 | """ 69 | warnings.warn( 70 | "argon2.hash_password_raw" + _INSTEAD, DeprecationWarning, stacklevel=2 71 | ) 72 | if salt is None: 73 | salt = os.urandom(DEFAULT_RANDOM_SALT_LENGTH) 74 | return hash_secret_raw( 75 | password, salt, time_cost, memory_cost, parallelism, hash_len, type 76 | ) 77 | 78 | 79 | def verify_password( 80 | hash: bytes, password: bytes, type: Type = Type.I 81 | ) -> Literal[True]: 82 | """ 83 | Legacy alias for :func:`argon2.low_level.verify_secret` with default 84 | parameters. 85 | 86 | .. deprecated:: 16.0.0 87 | Use :class:`argon2.PasswordHasher` for passwords. 88 | """ 89 | warnings.warn( 90 | "argon2.verify_password" + _INSTEAD, DeprecationWarning, stacklevel=2 91 | ) 92 | return verify_secret(hash, password, type) 93 | -------------------------------------------------------------------------------- /src/argon2/_password_hasher.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | 7 | from typing import ClassVar, Literal 8 | 9 | from ._utils import ( 10 | Parameters, 11 | _check_types, 12 | extract_parameters, 13 | validate_params_for_platform, 14 | ) 15 | from .exceptions import InvalidHashError 16 | from .low_level import Type, hash_secret, verify_secret 17 | from .profiles import get_default_parameters 18 | 19 | 20 | default_params = get_default_parameters() 21 | 22 | DEFAULT_RANDOM_SALT_LENGTH = default_params.salt_len 23 | DEFAULT_HASH_LENGTH = default_params.hash_len 24 | DEFAULT_TIME_COST = default_params.time_cost 25 | DEFAULT_MEMORY_COST = default_params.memory_cost 26 | DEFAULT_PARALLELISM = default_params.parallelism 27 | 28 | 29 | def _ensure_bytes(s: bytes | str, encoding: str) -> bytes: 30 | """ 31 | Ensure *s* is a bytes string. Encode using *encoding* if it isn't. 32 | """ 33 | if isinstance(s, bytes): 34 | return s 35 | return s.encode(encoding) 36 | 37 | 38 | class PasswordHasher: 39 | r""" 40 | High level class to hash passwords with sensible defaults. 41 | 42 | Uses Argon2\ **id** by default and uses a random salt_ for hashing. But it 43 | can verify any type of Argon2 as long as the hash is correctly encoded. 44 | 45 | The reason for this being a class is both for convenience to carry 46 | parameters and to verify the parameters only *once*. Any unnecessary 47 | slowdown when hashing is a tangible advantage for a brute-force attacker. 48 | 49 | Args: 50 | time_cost: 51 | Defines the amount of computation realized and therefore the 52 | execution time, given in number of iterations. 53 | 54 | memory_cost: Defines the memory usage, given in kibibytes_. 55 | 56 | parallelism: 57 | Defines the number of parallel threads (*changes* the resulting 58 | hash value). 59 | 60 | hash_len: Length of the hash in bytes. 61 | 62 | salt_len: Length of random salt to be generated for each password. 63 | 64 | encoding: 65 | The Argon2 C library expects bytes. So if :meth:`hash` or 66 | :meth:`verify` are passed a ``str``, it will be encoded using this 67 | encoding. 68 | 69 | type: 70 | Argon2 type to use. Only change for interoperability with legacy 71 | systems. 72 | 73 | .. versionadded:: 16.0.0 74 | .. versionchanged:: 18.2.0 75 | Switch from Argon2i to Argon2id based on the recommendation by the 76 | current RFC draft. See also :doc:`parameters`. 77 | .. versionchanged:: 18.2.0 78 | Changed default *memory_cost* to 100 MiB and default *parallelism* to 8. 79 | .. versionchanged:: 18.2.0 ``verify`` now will determine the type of hash. 80 | .. versionchanged:: 18.3.0 The Argon2 type is configurable now. 81 | .. versionadded:: 21.2.0 :meth:`from_parameters` 82 | .. versionchanged:: 21.2.0 83 | Changed defaults to :data:`argon2.profiles.RFC_9106_LOW_MEMORY`. 84 | 85 | .. _salt: https://en.wikipedia.org/wiki/Salt_(cryptography) 86 | .. _kibibytes: https://en.wikipedia.org/wiki/Binary_prefix#kibi 87 | """ 88 | 89 | __slots__ = ["_parameters", "encoding"] 90 | 91 | _parameters: Parameters 92 | encoding: str 93 | 94 | def __init__( 95 | self, 96 | time_cost: int = DEFAULT_TIME_COST, 97 | memory_cost: int = DEFAULT_MEMORY_COST, 98 | parallelism: int = DEFAULT_PARALLELISM, 99 | hash_len: int = DEFAULT_HASH_LENGTH, 100 | salt_len: int = DEFAULT_RANDOM_SALT_LENGTH, 101 | encoding: str = "utf-8", 102 | type: Type = Type.ID, 103 | ): 104 | e = _check_types( 105 | time_cost=(time_cost, int), 106 | memory_cost=(memory_cost, int), 107 | parallelism=(parallelism, int), 108 | hash_len=(hash_len, int), 109 | salt_len=(salt_len, int), 110 | encoding=(encoding, str), 111 | type=(type, Type), 112 | ) 113 | if e: 114 | raise TypeError(e) 115 | 116 | params = Parameters( 117 | type=type, 118 | version=19, 119 | salt_len=salt_len, 120 | hash_len=hash_len, 121 | time_cost=time_cost, 122 | memory_cost=memory_cost, 123 | parallelism=parallelism, 124 | ) 125 | 126 | validate_params_for_platform(params) 127 | 128 | # Cache a Parameters object for check_needs_rehash. 129 | self._parameters = params 130 | self.encoding = encoding 131 | 132 | @classmethod 133 | def from_parameters(cls, params: Parameters) -> PasswordHasher: 134 | """ 135 | Construct a `PasswordHasher` from *params*. 136 | 137 | Returns: 138 | A `PasswordHasher` instance with the parameters from *params*. 139 | 140 | .. versionadded:: 21.2.0 141 | """ 142 | 143 | return cls( 144 | time_cost=params.time_cost, 145 | memory_cost=params.memory_cost, 146 | parallelism=params.parallelism, 147 | hash_len=params.hash_len, 148 | salt_len=params.salt_len, 149 | type=params.type, 150 | ) 151 | 152 | @property 153 | def time_cost(self) -> int: 154 | return self._parameters.time_cost 155 | 156 | @property 157 | def memory_cost(self) -> int: 158 | return self._parameters.memory_cost 159 | 160 | @property 161 | def parallelism(self) -> int: 162 | return self._parameters.parallelism 163 | 164 | @property 165 | def hash_len(self) -> int: 166 | return self._parameters.hash_len 167 | 168 | @property 169 | def salt_len(self) -> int: 170 | return self._parameters.salt_len 171 | 172 | @property 173 | def type(self) -> Type: 174 | return self._parameters.type 175 | 176 | def hash(self, password: str | bytes, *, salt: bytes | None = None) -> str: 177 | """ 178 | Hash *password* and return an encoded hash. 179 | 180 | Args: 181 | password: Password to hash. 182 | 183 | salt: 184 | If None, a random salt is securely created. 185 | 186 | .. danger:: 187 | 188 | You should **not** pass a salt unless you really know what 189 | you are doing. 190 | 191 | Raises: 192 | argon2.exceptions.HashingError: If hashing fails. 193 | 194 | Returns: 195 | Hashed *password*. 196 | 197 | .. versionadded:: 23.1.0 *salt* parameter 198 | """ 199 | return hash_secret( 200 | secret=_ensure_bytes(password, self.encoding), 201 | salt=salt or os.urandom(self.salt_len), 202 | time_cost=self.time_cost, 203 | memory_cost=self.memory_cost, 204 | parallelism=self.parallelism, 205 | hash_len=self.hash_len, 206 | type=self.type, 207 | ).decode("ascii") 208 | 209 | _header_to_type: ClassVar[dict[bytes, Type]] = { 210 | b"$argon2i$": Type.I, 211 | b"$argon2d$": Type.D, 212 | b"$argon2id": Type.ID, 213 | } 214 | 215 | def verify( 216 | self, hash: str | bytes, password: str | bytes 217 | ) -> Literal[True]: 218 | """ 219 | Verify that *password* matches *hash*. 220 | 221 | .. warning:: 222 | 223 | It is assumed that the caller is in full control of the hash. No 224 | other parsing than the determination of the hash type is done by 225 | *argon2-cffi*. 226 | 227 | Args: 228 | hash: An encoded hash as returned from :meth:`PasswordHasher.hash`. 229 | 230 | password: The password to verify. 231 | 232 | Raises: 233 | argon2.exceptions.VerifyMismatchError: 234 | If verification fails because *hash* is not valid for 235 | *password*. 236 | 237 | argon2.exceptions.VerificationError: 238 | If verification fails for other reasons. 239 | 240 | argon2.exceptions.InvalidHashError: 241 | If *hash* is so clearly invalid, that it couldn't be passed to 242 | Argon2. 243 | 244 | Returns: 245 | ``True`` on success, otherwise an exception is raised. 246 | 247 | .. versionchanged:: 16.1.0 248 | Raise :exc:`~argon2.exceptions.VerifyMismatchError` on mismatches 249 | instead of its more generic superclass. 250 | .. versionadded:: 18.2.0 Hash type agility. 251 | """ 252 | hash = _ensure_bytes(hash, "ascii") 253 | try: 254 | hash_type = self._header_to_type[hash[:9]] 255 | except LookupError: 256 | raise InvalidHashError from None 257 | 258 | return verify_secret( 259 | hash, _ensure_bytes(password, self.encoding), hash_type 260 | ) 261 | 262 | def check_needs_rehash(self, hash: str | bytes) -> bool: 263 | """ 264 | Check whether *hash* was created using the instance's parameters. 265 | 266 | Whenever your Argon2 parameters -- or *argon2-cffi*'s defaults! -- 267 | change, you should rehash your passwords at the next opportunity. The 268 | common approach is to do that whenever a user logs in, since that 269 | should be the only time when you have access to the cleartext 270 | password. 271 | 272 | Therefore it's best practice to check -- and if necessary rehash -- 273 | passwords after each successful authentication. 274 | 275 | Args: 276 | hash: An encoded Argon2 password hash. 277 | 278 | Returns: 279 | Whether *hash* was created using the instance's parameters. 280 | 281 | .. versionadded:: 18.2.0 282 | .. versionchanged:: 24.1.0 Accepts bytes for *hash*. 283 | """ 284 | if isinstance(hash, bytes): 285 | hash = hash.decode("ascii") 286 | 287 | return self._parameters != extract_parameters(hash) 288 | -------------------------------------------------------------------------------- /src/argon2/_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | from __future__ import annotations 4 | 5 | import platform 6 | import sys 7 | 8 | from dataclasses import dataclass 9 | from typing import Any 10 | 11 | from .exceptions import InvalidHashError, UnsupportedParametersError 12 | from .low_level import Type 13 | 14 | 15 | NoneType = type(None) 16 | 17 | 18 | def _check_types(**kw: Any) -> str | None: 19 | """ 20 | Check each ``name: (value, types)`` in *kw*. 21 | 22 | Returns a human-readable string of all violations or `None``. 23 | """ 24 | errors = [] 25 | for name, (value, types) in kw.items(): 26 | if not isinstance(value, types): 27 | if isinstance(types, tuple): 28 | types = ", or ".join(t.__name__ for t in types) 29 | else: 30 | types = types.__name__ 31 | errors.append( 32 | f"'{name}' must be a {types} (got {type(value).__name__})" 33 | ) 34 | 35 | if errors != []: 36 | return ", ".join(errors) + "." 37 | 38 | return None 39 | 40 | 41 | def _is_wasm() -> bool: 42 | return sys.platform == "emscripten" or platform.machine() in [ 43 | "wasm32", 44 | "wasm64", 45 | ] 46 | 47 | 48 | def _decoded_str_len(length: int) -> int: 49 | """ 50 | Compute how long an encoded string of length *l* becomes. 51 | """ 52 | rem = length % 4 53 | 54 | if rem == 3: 55 | last_group_len = 2 56 | elif rem == 2: 57 | last_group_len = 1 58 | else: 59 | last_group_len = 0 60 | 61 | return length // 4 * 3 + last_group_len 62 | 63 | 64 | @dataclass 65 | class Parameters: 66 | """ 67 | Argon2 hash parameters. 68 | 69 | See :doc:`parameters` on how to pick them. 70 | 71 | Attributes: 72 | type: Hash type. 73 | 74 | version: Argon2 version. 75 | 76 | salt_len: Length of the salt in bytes. 77 | 78 | hash_len: Length of the hash in bytes. 79 | 80 | time_cost: Time cost in iterations. 81 | 82 | memory_cost: Memory cost in kibibytes. 83 | 84 | parallelism: Number of parallel threads. 85 | 86 | .. versionadded:: 18.2.0 87 | """ 88 | 89 | type: Type 90 | version: int 91 | salt_len: int 92 | hash_len: int 93 | time_cost: int 94 | memory_cost: int 95 | parallelism: int 96 | 97 | __slots__ = ( 98 | "hash_len", 99 | "memory_cost", 100 | "parallelism", 101 | "salt_len", 102 | "time_cost", 103 | "type", 104 | "version", 105 | ) 106 | 107 | 108 | _NAME_TO_TYPE = {"argon2id": Type.ID, "argon2i": Type.I, "argon2d": Type.D} 109 | _REQUIRED_KEYS = sorted(("v", "m", "t", "p")) 110 | 111 | 112 | def extract_parameters(hash: str) -> Parameters: 113 | """ 114 | Extract parameters from an encoded *hash*. 115 | 116 | Args: 117 | hash: An encoded Argon2 hash string. 118 | 119 | Returns: 120 | The parameters used to create the hash. 121 | 122 | .. versionadded:: 18.2.0 123 | """ 124 | parts = hash.split("$") 125 | 126 | # Backwards compatibility for Argon v1.2 hashes 127 | if len(parts) == 5: 128 | parts.insert(2, "v=18") 129 | 130 | if len(parts) != 6: 131 | raise InvalidHashError 132 | 133 | if parts[0]: 134 | raise InvalidHashError 135 | 136 | try: 137 | type = _NAME_TO_TYPE[parts[1]] 138 | 139 | kvs = { 140 | k: int(v) 141 | for k, v in ( 142 | s.split("=") for s in [parts[2], *parts[3].split(",")] 143 | ) 144 | } 145 | except Exception: # noqa: BLE001 146 | raise InvalidHashError from None 147 | 148 | if sorted(kvs.keys()) != _REQUIRED_KEYS: 149 | raise InvalidHashError 150 | 151 | return Parameters( 152 | type=type, 153 | salt_len=_decoded_str_len(len(parts[4])), 154 | hash_len=_decoded_str_len(len(parts[5])), 155 | version=kvs["v"], 156 | time_cost=kvs["t"], 157 | memory_cost=kvs["m"], 158 | parallelism=kvs["p"], 159 | ) 160 | 161 | 162 | def validate_params_for_platform(params: Parameters) -> None: 163 | """ 164 | Validate *params* against current platform. 165 | 166 | Args: 167 | params: Parameters to be validated 168 | 169 | Returns: 170 | None 171 | """ 172 | if _is_wasm() and params.parallelism != 1: 173 | msg = "In WebAssembly environments `parallelism` must be 1." 174 | raise UnsupportedParametersError(msg) 175 | -------------------------------------------------------------------------------- /src/argon2/exceptions.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | from __future__ import annotations 4 | 5 | 6 | class Argon2Error(Exception): 7 | """ 8 | Superclass of all argon2 exceptions. 9 | 10 | Never thrown directly. 11 | """ 12 | 13 | 14 | class VerificationError(Argon2Error): 15 | """ 16 | Verification failed. 17 | 18 | You can find the original error message from Argon2 in ``args[0]``. 19 | """ 20 | 21 | 22 | class VerifyMismatchError(VerificationError): 23 | """ 24 | The secret does not match the hash. 25 | 26 | Subclass of :exc:`argon2.exceptions.VerificationError`. 27 | 28 | .. versionadded:: 16.1.0 29 | """ 30 | 31 | 32 | class HashingError(Argon2Error): 33 | """ 34 | Raised if hashing failed. 35 | 36 | You can find the original error message from Argon2 in ``args[0]``. 37 | """ 38 | 39 | 40 | class InvalidHashError(ValueError): 41 | """ 42 | Raised if the hash is invalid before passing it to Argon2. 43 | 44 | .. versionadded:: 23.1.0 45 | As a replacement for :exc:`argon2.exceptions.InvalidHash`. 46 | """ 47 | 48 | 49 | class UnsupportedParametersError(ValueError): 50 | """ 51 | Raised if the current platform does not support the parameters. 52 | 53 | For example, in WebAssembly parallelism must be set to 1. 54 | 55 | .. versionadded:: 25.1.0 56 | """ 57 | 58 | 59 | InvalidHash = InvalidHashError 60 | """ 61 | Deprecated alias for :class:`InvalidHashError`. 62 | 63 | .. versionadded:: 18.2.0 64 | .. deprecated:: 23.1.0 65 | Use :exc:`argon2.exceptions.InvalidHashError` instead. 66 | """ 67 | -------------------------------------------------------------------------------- /src/argon2/low_level.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | """ 4 | Low-level functions if you want to build your own higher level abstractions. 5 | 6 | .. warning:: 7 | This is a "Hazardous Materials" module. You should **ONLY** use it if 8 | you're 100% absolutely sure that you know what you're doing because this 9 | module is full of land mines, dragons, and dinosaurs with laser guns. 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from enum import Enum 15 | from typing import Any, Literal 16 | 17 | from _argon2_cffi_bindings import ffi, lib 18 | 19 | from .exceptions import HashingError, VerificationError, VerifyMismatchError 20 | 21 | 22 | __all__ = [ 23 | "ARGON2_VERSION", 24 | "Type", 25 | "ffi", 26 | "hash_secret", 27 | "hash_secret_raw", 28 | "verify_secret", 29 | ] 30 | 31 | ARGON2_VERSION = lib.ARGON2_VERSION_NUMBER 32 | """ 33 | The latest version of the Argon2 algorithm that is supported (and used by 34 | default). 35 | 36 | .. versionadded:: 16.1.0 37 | """ 38 | 39 | 40 | class Type(Enum): 41 | """ 42 | Enum of Argon2 variants. 43 | 44 | Please see :doc:`parameters` on how to pick one. 45 | """ 46 | 47 | D = lib.Argon2_d 48 | I = lib.Argon2_i # noqa: E741 49 | ID = lib.Argon2_id 50 | 51 | 52 | def hash_secret( 53 | secret: bytes, 54 | salt: bytes, 55 | time_cost: int, 56 | memory_cost: int, 57 | parallelism: int, 58 | hash_len: int, 59 | type: Type, 60 | version: int = ARGON2_VERSION, 61 | ) -> bytes: 62 | """ 63 | Hash *secret* and return an **encoded** hash. 64 | 65 | An encoded hash can be directly passed into :func:`verify_secret` as it 66 | contains all parameters and the salt. 67 | 68 | Args: 69 | secret: Secret to hash. 70 | 71 | salt: A salt_. Should be random and different for each secret. 72 | 73 | type: Which Argon2 variant to use. 74 | 75 | version: Which Argon2 version to use. 76 | 77 | For an explanation of the Argon2 parameters see 78 | :class:`argon2.PasswordHasher`. 79 | 80 | Returns: 81 | An encoded Argon2 hash. 82 | 83 | Raises: 84 | argon2.exceptions.HashingError: If hashing fails. 85 | 86 | .. versionadded:: 16.0.0 87 | 88 | .. _salt: https://en.wikipedia.org/wiki/Salt_(cryptography) 89 | """ 90 | size = ( 91 | lib.argon2_encodedlen( 92 | time_cost, 93 | memory_cost, 94 | parallelism, 95 | len(salt), 96 | hash_len, 97 | type.value, 98 | ) 99 | + 1 100 | ) 101 | buf = ffi.new("char[]", size) 102 | rv = lib.argon2_hash( 103 | time_cost, 104 | memory_cost, 105 | parallelism, 106 | ffi.new("uint8_t[]", secret), 107 | len(secret), 108 | ffi.new("uint8_t[]", salt), 109 | len(salt), 110 | ffi.NULL, 111 | hash_len, 112 | buf, 113 | size, 114 | type.value, 115 | version, 116 | ) 117 | if rv != lib.ARGON2_OK: 118 | raise HashingError(error_to_str(rv)) 119 | 120 | return ffi.string(buf) # type: ignore[no-any-return] 121 | 122 | 123 | def hash_secret_raw( 124 | secret: bytes, 125 | salt: bytes, 126 | time_cost: int, 127 | memory_cost: int, 128 | parallelism: int, 129 | hash_len: int, 130 | type: Type, 131 | version: int = ARGON2_VERSION, 132 | ) -> bytes: 133 | """ 134 | Hash *password* and return a **raw** hash. 135 | 136 | This function takes the same parameters as :func:`hash_secret`. 137 | 138 | .. versionadded:: 16.0.0 139 | """ 140 | buf = ffi.new("uint8_t[]", hash_len) 141 | 142 | rv = lib.argon2_hash( 143 | time_cost, 144 | memory_cost, 145 | parallelism, 146 | ffi.new("uint8_t[]", secret), 147 | len(secret), 148 | ffi.new("uint8_t[]", salt), 149 | len(salt), 150 | buf, 151 | hash_len, 152 | ffi.NULL, 153 | 0, 154 | type.value, 155 | version, 156 | ) 157 | if rv != lib.ARGON2_OK: 158 | raise HashingError(error_to_str(rv)) 159 | 160 | return bytes(ffi.buffer(buf, hash_len)) 161 | 162 | 163 | def verify_secret(hash: bytes, secret: bytes, type: Type) -> Literal[True]: 164 | """ 165 | Verify whether *secret* is correct for *hash* of *type*. 166 | 167 | Args: 168 | hash: 169 | An encoded Argon2 hash as returned by :func:`hash_secret`. 170 | 171 | secret: 172 | The secret to verify whether it matches the one in *hash*. 173 | 174 | type: Type for *hash*. 175 | 176 | Raises: 177 | argon2.exceptions.VerifyMismatchError: 178 | If verification fails because *hash* is not valid for *secret* of 179 | *type*. 180 | 181 | argon2.exceptions.VerificationError: 182 | If verification fails for other reasons. 183 | 184 | Returns: 185 | ``True`` on success, raise :exc:`~argon2.exceptions.VerificationError` 186 | otherwise. 187 | 188 | .. versionadded:: 16.0.0 189 | .. versionchanged:: 16.1.0 190 | Raise :exc:`~argon2.exceptions.VerifyMismatchError` on mismatches 191 | instead of its more generic superclass. 192 | """ 193 | rv = lib.argon2_verify( 194 | ffi.new("char[]", hash), 195 | ffi.new("uint8_t[]", secret), 196 | len(secret), 197 | type.value, 198 | ) 199 | 200 | if rv == lib.ARGON2_OK: 201 | return True 202 | 203 | if rv == lib.ARGON2_VERIFY_MISMATCH: 204 | raise VerifyMismatchError(error_to_str(rv)) 205 | 206 | raise VerificationError(error_to_str(rv)) 207 | 208 | 209 | def core(context: Any, type: int) -> int: 210 | """ 211 | Direct binding to the ``argon2_ctx`` function. 212 | 213 | .. warning:: 214 | This is a strictly advanced function working on raw C data structures. 215 | Both Argon2's and *argon2-cffi*'s higher-level bindings do a lot of 216 | sanity checks and housekeeping work that *you* are now responsible for 217 | (e.g. clearing buffers). The structure of the *context* object can, 218 | has, and will change with *any* release! 219 | 220 | Use at your own peril; *argon2-cffi* does *not* use this binding 221 | itself. 222 | 223 | Args: 224 | context: 225 | A CFFI Argon2 context object (i.e. an ``struct Argon2_Context`` / 226 | ``argon2_context``). 227 | 228 | type: 229 | Which Argon2 variant to use. You can use the ``value`` field of 230 | :class:`Type`'s fields. 231 | 232 | Returns: 233 | An Argon2 error code. Can be transformed into a string using 234 | :func:`error_to_str`. 235 | 236 | .. versionadded:: 16.0.0 237 | """ 238 | return lib.argon2_ctx(context, type) # type: ignore[no-any-return] 239 | 240 | 241 | def error_to_str(error: int) -> str: 242 | """ 243 | Convert an Argon2 error code into a native string. 244 | 245 | Args: 246 | error: An Argon2 error code as returned by :func:`core`. 247 | 248 | Returns: 249 | A human-readable string describing the error. 250 | 251 | .. versionadded:: 16.0.0 252 | """ 253 | return ffi.string(lib.argon2_error_message(error)).decode("ascii") # type: ignore[no-any-return] 254 | -------------------------------------------------------------------------------- /src/argon2/profiles.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | """ 4 | This module offers access to standardized parameters that you can load using 5 | :meth:`argon2.PasswordHasher.from_parameters()`. See the `source code 6 | `_ for 7 | concrete values and :doc:`parameters` for more information. 8 | 9 | .. versionadded:: 21.2.0 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | import dataclasses 15 | 16 | from ._utils import Parameters, _is_wasm 17 | from .low_level import Type 18 | 19 | 20 | def get_default_parameters() -> Parameters: 21 | """ 22 | Create default parameters for current platform. 23 | 24 | Returns: 25 | Default, compatible, parameters for current platform. 26 | 27 | .. versionadded:: 25.1.0 28 | """ 29 | params = RFC_9106_LOW_MEMORY 30 | 31 | if _is_wasm(): 32 | params = dataclasses.replace(params, parallelism=1) 33 | 34 | return params 35 | 36 | 37 | # FIRST RECOMMENDED option per RFC 9106. 38 | RFC_9106_HIGH_MEMORY = Parameters( 39 | type=Type.ID, 40 | version=19, 41 | salt_len=16, 42 | hash_len=32, 43 | time_cost=1, 44 | memory_cost=2097152, # 2 GiB 45 | parallelism=4, 46 | ) 47 | 48 | # SECOND RECOMMENDED option per RFC 9106. 49 | RFC_9106_LOW_MEMORY = Parameters( 50 | type=Type.ID, 51 | version=19, 52 | salt_len=16, 53 | hash_len=32, 54 | time_cost=3, 55 | memory_cost=65536, # 64 MiB 56 | parallelism=4, 57 | ) 58 | 59 | # The pre-RFC defaults in argon2-cffi 18.2.0 - 21.1.0. 60 | PRE_21_2 = Parameters( 61 | type=Type.ID, 62 | version=19, 63 | salt_len=16, 64 | hash_len=16, 65 | time_cost=2, 66 | memory_cost=102400, # 100 MiB 67 | parallelism=8, 68 | ) 69 | 70 | # Only for testing! 71 | CHEAPEST = Parameters( 72 | type=Type.ID, 73 | version=19, 74 | salt_len=8, 75 | hash_len=4, 76 | time_cost=1, 77 | memory_cost=8, 78 | parallelism=1, 79 | ) 80 | -------------------------------------------------------------------------------- /src/argon2/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynek/argon2-cffi/16476222ca3e7b860c9d51fd5287e4726d8996d6/src/argon2/py.typed -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynek/argon2-cffi/16476222ca3e7b860c9d51fd5287e4726d8996d6/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_legacy.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import pytest 4 | 5 | from hypothesis import given 6 | from hypothesis import strategies as st 7 | 8 | from argon2 import ( 9 | DEFAULT_RANDOM_SALT_LENGTH, 10 | Type, 11 | hash_password, 12 | hash_password_raw, 13 | verify_password, 14 | ) 15 | from argon2.exceptions import HashingError, VerificationError 16 | 17 | from .test_low_level import ( 18 | TEST_HASH_I, 19 | TEST_HASH_LEN, 20 | TEST_MEMORY, 21 | TEST_PARALLELISM, 22 | TEST_PASSWORD, 23 | TEST_SALT, 24 | TEST_TIME, 25 | i_and_d_encoded, 26 | i_and_d_raw, 27 | ) 28 | 29 | 30 | class TestHash: 31 | def test_hash_defaults(self): 32 | """ 33 | Calling without arguments works. 34 | """ 35 | with pytest.deprecated_call( 36 | match="argon2.hash_password is deprecated" 37 | ) as dc: 38 | hash_password(b"secret") 39 | 40 | assert dc.pop().filename.endswith("test_legacy.py") 41 | 42 | def test_raw_defaults(self): 43 | """ 44 | Calling without arguments works. 45 | """ 46 | with pytest.deprecated_call( 47 | match="argon2.hash_password_raw is deprecated" 48 | ) as dc: 49 | hash_password_raw(b"secret") 50 | 51 | assert dc.pop().filename.endswith("test_legacy.py") 52 | 53 | @i_and_d_encoded 54 | def test_hash_password(self, type, hash): 55 | """ 56 | Creates the same encoded hash as the Argon2 CLI client. 57 | """ 58 | with pytest.deprecated_call( 59 | match="argon2.hash_password is deprecated" 60 | ): 61 | rv = hash_password( 62 | TEST_PASSWORD, 63 | TEST_SALT, 64 | TEST_TIME, 65 | TEST_MEMORY, 66 | TEST_PARALLELISM, 67 | TEST_HASH_LEN, 68 | type, 69 | ) 70 | 71 | assert hash == rv 72 | assert isinstance(rv, bytes) 73 | 74 | @i_and_d_raw 75 | def test_hash_password_raw(self, type, hash): 76 | """ 77 | Creates the same raw hash as the Argon2 CLI client. 78 | """ 79 | with pytest.deprecated_call( 80 | match="argon2.hash_password_raw is deprecated" 81 | ): 82 | rv = hash_password_raw( 83 | TEST_PASSWORD, 84 | TEST_SALT, 85 | TEST_TIME, 86 | TEST_MEMORY, 87 | TEST_PARALLELISM, 88 | TEST_HASH_LEN, 89 | type, 90 | ) 91 | 92 | assert hash == rv 93 | assert isinstance(rv, bytes) 94 | 95 | def test_hash_nul_bytes(self): 96 | """ 97 | Hashing passwords with NUL bytes works as expected. 98 | """ 99 | with pytest.deprecated_call( 100 | match="argon2.hash_password_raw is deprecated" 101 | ): 102 | rv = hash_password_raw(b"abc\x00", TEST_SALT) 103 | 104 | with pytest.deprecated_call( 105 | match="argon2.hash_password_raw is deprecated" 106 | ): 107 | assert rv != hash_password_raw(b"abc", TEST_SALT) 108 | 109 | def test_random_salt(self): 110 | """ 111 | Omitting a salt, creates a random one. 112 | """ 113 | with pytest.deprecated_call( 114 | match="argon2.hash_password is deprecated" 115 | ): 116 | rv = hash_password(b"secret") 117 | salt = rv.split(b",")[-1].split(b"$")[1] 118 | 119 | assert ( 120 | # -1 for not NUL byte 121 | int((DEFAULT_RANDOM_SALT_LENGTH << 2) / 3 + 2) - 1 == len(salt) 122 | ) 123 | 124 | def test_hash_wrong_arg_type(self): 125 | """ 126 | Passing an argument of wrong type raises TypeError. 127 | """ 128 | with pytest.deprecated_call( 129 | match="argon2.hash_password is deprecated" 130 | ), pytest.raises(TypeError): 131 | hash_password("oh no, unicode!") 132 | 133 | def test_illegal_argon2_parameter(self): 134 | """ 135 | Raises HashingError if hashing fails. 136 | """ 137 | with pytest.deprecated_call( 138 | match="argon2.hash_password is deprecated" 139 | ), pytest.raises(HashingError): 140 | hash_password(TEST_PASSWORD, memory_cost=1) 141 | 142 | @given(st.binary(max_size=128)) 143 | def test_hash_fast(self, password): 144 | """ 145 | Hash various passwords as cheaply as possible. 146 | """ 147 | with pytest.deprecated_call( 148 | match="argon2.hash_password is deprecated" 149 | ): 150 | hash_password( 151 | password, 152 | salt=b"12345678", 153 | time_cost=1, 154 | memory_cost=8, 155 | parallelism=1, 156 | hash_len=8, 157 | ) 158 | 159 | 160 | class TestVerify: 161 | @i_and_d_encoded 162 | def test_success(self, type, hash): 163 | """ 164 | Given a valid hash and password and correct type, we succeed. 165 | """ 166 | with pytest.deprecated_call( 167 | match="argon2.verify_password is deprecated" 168 | ) as dc: 169 | assert True is verify_password(hash, TEST_PASSWORD, type) 170 | 171 | assert dc.pop().filename.endswith("test_legacy.py") 172 | 173 | def test_fail_wrong_argon2_type(self): 174 | """ 175 | Given a valid hash and password and wrong type, we fail. 176 | """ 177 | with pytest.deprecated_call( 178 | match="argon2.verify_password is deprecated" 179 | ), pytest.raises(VerificationError): 180 | verify_password(TEST_HASH_I, TEST_PASSWORD, Type.D) 181 | 182 | def test_wrong_arg_type(self): 183 | """ 184 | Passing an argument of wrong type raises TypeError. 185 | """ 186 | with pytest.deprecated_call( 187 | match="argon2.verify_password is deprecated" 188 | ), pytest.raises(TypeError): 189 | verify_password(TEST_HASH_I, TEST_PASSWORD.decode("ascii")) 190 | -------------------------------------------------------------------------------- /tests/test_low_level.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import binascii 4 | import os 5 | 6 | import pytest 7 | 8 | from hypothesis import assume, given, settings 9 | from hypothesis import strategies as st 10 | 11 | from argon2.exceptions import ( 12 | HashingError, 13 | VerificationError, 14 | VerifyMismatchError, 15 | ) 16 | from argon2.low_level import ( 17 | ARGON2_VERSION, 18 | Type, 19 | core, 20 | ffi, 21 | hash_secret, 22 | hash_secret_raw, 23 | lib, 24 | verify_secret, 25 | ) 26 | 27 | 28 | # Example data obtained using the official Argon2 CLI client: 29 | # 30 | # $ echo -n "password" | ./argon2 somesalt -t 2 -m 16 -p 4 31 | # Type: Argon2i 32 | # Iterations: 2 33 | # Memory: 65536 KiB 34 | # Parallelism: 4 35 | # Hash: 20c8adf6a90550b08c03f5628b32f9edc9d32ce6b90e254cf5e330a40bcfc2be 36 | # Encoded: $argon2i$v=19$m=65536,t=2,p=4$ 37 | # c29tZXNhbHQ$IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4 38 | # 0.120 seconds 39 | # Verification ok 40 | # 41 | # $ echo -n "password" | ./argon2 somesalt -t 2 -m 16 -p 4 -d 42 | # Type: Argon2d 43 | # Iterations: 2 44 | # Memory: 65536 KiB 45 | # Parallelism: 4 46 | # Hash: 7199f977eac587e65fb91866da21941a072b5b960b78ceaaecbdef06c766140d 47 | # Encoded: $argon2d$v=19$m=65536,t=2,p=4$ 48 | # c29tZXNhbHQ$cZn5d+rFh+ZfuRhm2iGUGgcrW5YLeM6q7L3vBsdmFA0 49 | # 0.119 seconds 50 | # Verification ok 51 | # 52 | # Type: Argon2id 53 | # Iterations: 2 54 | # Memory: 65536 KiB 55 | # Parallelism: 4 56 | # Hash: 1a9677b0afe81fda7b548895e7a1bfeb8668ffc19a530e37e088a668fab1c02a 57 | # Encoded: $argon2id$v=19$m=65536,t=2,p=4$ 58 | # c29tZXNhbHQ$GpZ3sK/oH9p7VIiV56G/64Zo/8GaUw434IimaPqxwCo 59 | # 0.154 seconds 60 | # Verification ok 61 | 62 | TEST_HASH_I_OLD = ( 63 | b"$argon2i$m=65536,t=2,p=4" 64 | b"$c29tZXNhbHQAAAAAAAAAAA" 65 | b"$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY" 66 | ) # a v1.2 hash without a version tag 67 | TEST_HASH_I = ( 68 | b"$argon2i$v=19$m=65536,t=2,p=4$" 69 | b"c29tZXNhbHQ$IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4" 70 | ) 71 | TEST_HASH_D = ( 72 | b"$argon2d$v=19$m=65536,t=2,p=4$" 73 | b"c29tZXNhbHQ$cZn5d+rFh+ZfuRhm2iGUGgcrW5YLeM6q7L3vBsdmFA0" 74 | ) 75 | TEST_HASH_ID = ( 76 | b"$argon2id$v=19$m=65536,t=2,p=4$" 77 | b"c29tZXNhbHQ$GpZ3sK/oH9p7VIiV56G/64Zo/8GaUw434IimaPqxwCo" 78 | ) 79 | TEST_RAW_I = binascii.unhexlify( 80 | b"20c8adf6a90550b08c03f5628b32f9edc9d32ce6b90e254cf5e330a40bcfc2be" 81 | ) 82 | TEST_RAW_D = binascii.unhexlify( 83 | b"7199f977eac587e65fb91866da21941a072b5b960b78ceaaecbdef06c766140d" 84 | ) 85 | TEST_RAW_ID = binascii.unhexlify( 86 | b"1a9677b0afe81fda7b548895e7a1bfeb8668ffc19a530e37e088a668fab1c02a" 87 | ) 88 | 89 | TEST_PASSWORD = b"password" 90 | TEST_SALT_LEN = 16 91 | TEST_SALT = b"somesalt" 92 | TEST_TIME = 2 93 | TEST_MEMORY = 65536 94 | TEST_PARALLELISM = 4 95 | TEST_HASH_LEN = 32 96 | 97 | i_and_d_encoded = pytest.mark.parametrize( 98 | ("type", "hash"), 99 | [(Type.I, TEST_HASH_I), (Type.D, TEST_HASH_D), (Type.ID, TEST_HASH_ID)], 100 | ) 101 | i_and_d_raw = pytest.mark.parametrize( 102 | ("type", "hash"), 103 | [(Type.I, TEST_RAW_I), (Type.D, TEST_RAW_D), (Type.ID, TEST_RAW_ID)], 104 | ) 105 | 106 | both_hash_funcs = pytest.mark.parametrize( 107 | "func", [hash_secret, hash_secret_raw] 108 | ) 109 | 110 | 111 | class TestHash: 112 | @i_and_d_encoded 113 | def test_hash_secret(self, type, hash): 114 | """ 115 | Creates the same encoded hash as the Argon2 CLI client. 116 | """ 117 | rv = hash_secret( 118 | TEST_PASSWORD, 119 | TEST_SALT, 120 | TEST_TIME, 121 | TEST_MEMORY, 122 | TEST_PARALLELISM, 123 | TEST_HASH_LEN, 124 | type, 125 | ) 126 | 127 | assert hash == rv 128 | assert isinstance(rv, bytes) 129 | 130 | @i_and_d_raw 131 | def test_hash_secret_raw(self, type, hash): 132 | """ 133 | Creates the same raw hash as the Argon2 CLI client. 134 | """ 135 | rv = hash_secret_raw( 136 | TEST_PASSWORD, 137 | TEST_SALT, 138 | TEST_TIME, 139 | TEST_MEMORY, 140 | TEST_PARALLELISM, 141 | TEST_HASH_LEN, 142 | type, 143 | ) 144 | 145 | assert hash == rv 146 | assert isinstance(rv, bytes) 147 | 148 | def test_hash_nul_bytes(self): 149 | """ 150 | Hashing secrets with NUL bytes works as expected. 151 | """ 152 | params = ( 153 | TEST_SALT, 154 | TEST_TIME, 155 | TEST_MEMORY, 156 | TEST_PARALLELISM, 157 | TEST_HASH_LEN, 158 | Type.I, 159 | ) 160 | rv = hash_secret_raw(b"abc\x00", *params) 161 | 162 | assert rv != hash_secret_raw(b"abc", *params) 163 | 164 | @both_hash_funcs 165 | def test_hash_wrong_arg_type(self, func): 166 | """ 167 | Passing an argument of wrong type raises TypeError. 168 | """ 169 | with pytest.raises(TypeError): 170 | func("oh no, unicode!") 171 | 172 | @both_hash_funcs 173 | def test_illegal_argon2_parameter(self, func): 174 | """ 175 | Raises HashingError if hashing fails. 176 | """ 177 | with pytest.raises(HashingError): 178 | func( 179 | TEST_PASSWORD, 180 | TEST_SALT, 181 | TEST_TIME, 182 | 1, 183 | TEST_PARALLELISM, 184 | TEST_HASH_LEN, 185 | Type.I, 186 | ) 187 | 188 | @given( 189 | st.sampled_from((hash_secret, hash_secret_raw)), 190 | st.binary(max_size=128), 191 | ) 192 | def test_hash_fast(self, func, secret): 193 | """ 194 | Hash various secrets as cheaply as possible. 195 | """ 196 | func( 197 | secret, 198 | salt=b"12345678", 199 | time_cost=1, 200 | memory_cost=8, 201 | parallelism=1, 202 | hash_len=8, 203 | type=Type.I, 204 | ) 205 | 206 | 207 | class TestVerify: 208 | @i_and_d_encoded 209 | def test_success(self, type, hash): 210 | """ 211 | Given a valid hash and secret and correct type, we succeed. 212 | """ 213 | assert True is verify_secret(hash, TEST_PASSWORD, type) 214 | 215 | @i_and_d_encoded 216 | def test_fail(self, type, hash): 217 | """ 218 | Wrong password fails. 219 | """ 220 | with pytest.raises(VerifyMismatchError): 221 | verify_secret(hash, bytes(reversed(TEST_PASSWORD)), type) 222 | 223 | def test_fail_wrong_argon2_type(self): 224 | """ 225 | Given a valid hash and secret and wrong type, we fail. 226 | """ 227 | verify_secret(TEST_HASH_D, TEST_PASSWORD, Type.D) 228 | verify_secret(TEST_HASH_I, TEST_PASSWORD, Type.I) 229 | with pytest.raises(VerificationError): 230 | verify_secret(TEST_HASH_I, TEST_PASSWORD, Type.D) 231 | 232 | def test_wrong_arg_type(self): 233 | """ 234 | Passing an argument of wrong type raises TypeError. 235 | """ 236 | with pytest.raises(TypeError) as e: 237 | verify_secret(TEST_HASH_I, TEST_PASSWORD.decode("ascii"), Type.I) 238 | 239 | assert e.value.args[0].startswith( 240 | "initializer for ctype 'uint8_t[]' must be a" 241 | ) 242 | 243 | def test_old_hash(self): 244 | """ 245 | Hashes without a version tag are recognized and verified correctly. 246 | """ 247 | assert True is verify_secret(TEST_HASH_I_OLD, TEST_PASSWORD, Type.I) 248 | 249 | 250 | @given( 251 | password=st.binary(min_size=lib.ARGON2_MIN_PWD_LENGTH, max_size=65), 252 | time_cost=st.integers(lib.ARGON2_MIN_TIME, 3), 253 | parallelism=st.integers(lib.ARGON2_MIN_LANES, 5), 254 | memory_cost=st.integers(0, 1025), 255 | hash_len=st.integers(lib.ARGON2_MIN_OUTLEN, 513), 256 | salt_len=st.integers(lib.ARGON2_MIN_SALT_LENGTH, 513), 257 | ) 258 | @settings(deadline=None) 259 | def test_argument_ranges( 260 | password, time_cost, parallelism, memory_cost, hash_len, salt_len 261 | ): 262 | """ 263 | Ensure that both hashing and verifying works for most combinations of legal 264 | values. 265 | 266 | Limits are intentionally chosen to be *not* on 2^x boundaries. 267 | 268 | This test is rather slow. 269 | """ 270 | assume(parallelism * 8 <= memory_cost) 271 | hash = hash_secret( 272 | secret=password, 273 | salt=os.urandom(salt_len), 274 | time_cost=time_cost, 275 | parallelism=parallelism, 276 | memory_cost=memory_cost, 277 | hash_len=hash_len, 278 | type=Type.I, 279 | ) 280 | assert verify_secret(hash, password, Type.I) 281 | 282 | 283 | def test_core(): 284 | """ 285 | If called with equal parameters, core() will return the same as 286 | hash_secret(). 287 | """ 288 | pwd = b"secret" 289 | salt = b"12345678" 290 | hash_len = 8 291 | 292 | # Keep FFI objects alive throughout the function. 293 | cout = ffi.new("uint8_t[]", hash_len) 294 | cpwd = ffi.new("uint8_t[]", pwd) 295 | csalt = ffi.new("uint8_t[]", salt) 296 | 297 | ctx = ffi.new( 298 | "argon2_context *", 299 | { 300 | "out": cout, 301 | "outlen": hash_len, 302 | "version": ARGON2_VERSION, 303 | "pwd": cpwd, 304 | "pwdlen": len(pwd), 305 | "salt": csalt, 306 | "saltlen": len(salt), 307 | "secret": ffi.NULL, 308 | "secretlen": 0, 309 | "ad": ffi.NULL, 310 | "adlen": 0, 311 | "t_cost": 1, 312 | "m_cost": 8, 313 | "lanes": 1, 314 | "threads": 1, 315 | "allocate_cbk": ffi.NULL, 316 | "free_cbk": ffi.NULL, 317 | "flags": lib.ARGON2_DEFAULT_FLAGS, 318 | }, 319 | ) 320 | 321 | rv = core(ctx, Type.D.value) 322 | 323 | assert 0 == rv 324 | assert hash_secret_raw( 325 | pwd, 326 | salt=salt, 327 | time_cost=1, 328 | memory_cost=8, 329 | parallelism=1, 330 | hash_len=hash_len, 331 | type=Type.D, 332 | ) == bytes(ffi.buffer(ctx.out, ctx.outlen)) 333 | -------------------------------------------------------------------------------- /tests/test_packaging.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | 4 | from importlib import metadata 5 | 6 | import pytest 7 | 8 | import argon2 9 | 10 | 11 | class TestLegacyMetadataHack: 12 | def test_version(self): 13 | """ 14 | argon2.__version__ returns the correct version. 15 | """ 16 | with pytest.deprecated_call(): 17 | assert metadata.version("argon2-cffi") == argon2.__version__ 18 | 19 | def test_description(self): 20 | """ 21 | argon2.__description__ returns the correct description. 22 | """ 23 | with pytest.deprecated_call(): 24 | assert "Argon2 for Python" == argon2.__description__ 25 | 26 | def test_uri(self): 27 | """ 28 | argon2.__uri__ returns the correct project URL. 29 | """ 30 | with pytest.deprecated_call(): 31 | assert "https://argon2-cffi.readthedocs.io/" == argon2.__uri__ 32 | 33 | with pytest.deprecated_call(): 34 | assert "https://argon2-cffi.readthedocs.io/" == argon2.__url__ 35 | 36 | def test_email(self): 37 | """ 38 | argon2.__email__ returns Hynek's email address. 39 | """ 40 | with pytest.deprecated_call(): 41 | assert "hs@ox.cx" == argon2.__email__ 42 | 43 | def test_does_not_exist(self): 44 | """ 45 | Asking for unsupported dunders raises an AttributeError. 46 | """ 47 | with pytest.raises( 48 | AttributeError, match="module argon2 has no attribute __yolo__" 49 | ): 50 | argon2.__yolo__ # noqa: B018 51 | -------------------------------------------------------------------------------- /tests/test_password_hasher.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | import secrets 4 | import sys 5 | import threading 6 | 7 | from concurrent.futures import ThreadPoolExecutor 8 | from unittest import mock 9 | 10 | import pytest 11 | 12 | from argon2 import PasswordHasher, Type, extract_parameters, profiles 13 | from argon2._password_hasher import _ensure_bytes 14 | from argon2._utils import Parameters 15 | from argon2.exceptions import ( 16 | InvalidHash, 17 | InvalidHashError, 18 | UnsupportedParametersError, 19 | ) 20 | 21 | 22 | class TestEnsureBytes: 23 | def test_is_bytes(self): 24 | """ 25 | Bytes are just returned. 26 | """ 27 | s = "föö".encode() 28 | 29 | rv = _ensure_bytes(s, "doesntmatter") 30 | 31 | assert isinstance(rv, bytes) 32 | assert s == rv 33 | 34 | def test_is_str(self): 35 | """ 36 | Unicode str is encoded using the specified encoding. 37 | """ 38 | s = "föö" 39 | 40 | rv = _ensure_bytes(s, "latin1") 41 | 42 | assert isinstance(rv, bytes) 43 | assert s.encode("latin1") == rv 44 | 45 | 46 | bytes_and_str_password = pytest.mark.parametrize( 47 | "password", ["pässword".encode("latin1"), "pässword"] 48 | ) 49 | 50 | 51 | class TestPasswordHasher: 52 | @bytes_and_str_password 53 | def test_hash(self, password): 54 | """ 55 | Hashing works with str and bytes. Uses correct parameters. 56 | """ 57 | ph = PasswordHasher(1, 8, 1, 16, 16, "latin1") 58 | 59 | h = ph.hash(password) 60 | 61 | prefix = "$argon2id$v=19$m=8,t=1,p=1$" 62 | 63 | assert isinstance(h, str) 64 | assert h[: len(prefix)] == prefix 65 | 66 | def test_custom_salt(self, password=b"password"): 67 | """ 68 | A custom salt can be specified. 69 | """ 70 | ph = PasswordHasher.from_parameters(profiles.CHEAPEST) 71 | 72 | h = ph.hash(password, salt=b"1234567890123456") 73 | 74 | assert h == ( 75 | "$argon2id$v=19$m=8,t=1,p=1$MTIzNDU2Nzg5MDEyMzQ1Ng$maTa5w" 76 | ) 77 | 78 | @bytes_and_str_password 79 | def test_verify_agility(self, password): 80 | """ 81 | Verification works with str and bytes and variant is correctly 82 | detected. 83 | """ 84 | ph = PasswordHasher(1, 8, 1, 16, 16, "latin1") 85 | hash = ( # handrolled artisanal test vector 86 | "$argon2i$m=8,t=1,p=1$" 87 | "bL/lLsegFKTuR+5vVyA8tA$VKz5CHavCtFOL1N5TIXWSA" 88 | ) 89 | 90 | assert ph.verify(hash, password) 91 | 92 | @bytes_and_str_password 93 | def test_hash_verify(self, password): 94 | """ 95 | Hashes are valid and can be verified. 96 | """ 97 | ph = PasswordHasher() 98 | 99 | assert ph.verify(ph.hash(password), password) is True 100 | 101 | def test_check(self): 102 | """ 103 | Raises a helpful TypeError on wrong arguments. 104 | """ 105 | with pytest.raises(TypeError) as e: 106 | PasswordHasher("1") 107 | 108 | assert "'time_cost' must be a int (got str)." == e.value.args[0] 109 | 110 | def test_verify_invalid_hash_error(self): 111 | """ 112 | If the hash can't be parsed, InvalidHashError is raised. 113 | """ 114 | with pytest.raises(InvalidHashError): 115 | PasswordHasher().verify("tiger", "does not matter") 116 | 117 | def test_verify_invalid_hash(self): 118 | """ 119 | InvalidHashError and the deprecrated InvalidHash are the same. 120 | """ 121 | with pytest.raises(InvalidHash): 122 | PasswordHasher().verify("tiger", "does not matter") 123 | 124 | @pytest.mark.parametrize("use_bytes", [True, False]) 125 | def test_check_needs_rehash_no(self, use_bytes): 126 | """ 127 | Return False if the hash has the correct parameters. 128 | """ 129 | ph = PasswordHasher(1, 8, 1, 16, 16) 130 | 131 | hash = ph.hash("foo") 132 | if use_bytes: 133 | hash = hash.encode() 134 | 135 | assert not ph.check_needs_rehash(hash) 136 | 137 | @pytest.mark.parametrize("use_bytes", [True, False]) 138 | def test_check_needs_rehash_yes(self, use_bytes): 139 | """ 140 | Return True if any of the parameters changes. 141 | """ 142 | ph = PasswordHasher(1, 8, 1, 16, 16) 143 | ph_old = PasswordHasher(1, 8, 1, 8, 8) 144 | 145 | hash = ph_old.hash("foo") 146 | if use_bytes: 147 | hash = hash.encode() 148 | 149 | assert ph.check_needs_rehash(hash) 150 | 151 | def test_type_is_configurable(self): 152 | """ 153 | Argon2id is default but can be changed. 154 | """ 155 | ph = PasswordHasher(time_cost=1, memory_cost=64) 156 | default_hash = ph.hash("foo") 157 | 158 | assert Type.ID is ph.type is ph._parameters.type 159 | assert Type.ID is extract_parameters(default_hash).type 160 | 161 | ph = PasswordHasher(time_cost=1, memory_cost=64, type=Type.I) 162 | 163 | assert Type.I is ph.type is ph._parameters.type 164 | assert Type.I is extract_parameters(ph.hash("foo")).type 165 | assert ph.check_needs_rehash(default_hash) 166 | 167 | @mock.patch("sys.platform", "emscripten") 168 | @pytest.mark.parametrize("machine", ["wasm32", "wasm64"]) 169 | def test_params_on_wasm(self, machine): 170 | """ 171 | Parameter validation catches invalid parameters on WebAssembly. 172 | """ 173 | with mock.patch("platform.machine", return_value=machine): 174 | with pytest.raises( 175 | UnsupportedParametersError, 176 | match="In WebAssembly environments `parallelism` must be 1.", 177 | ): 178 | PasswordHasher(parallelism=2) 179 | 180 | # last param is parallelism so it should fail 181 | params = Parameters(Type.I, 2, 8, 8, 3, 256, 8) 182 | with pytest.raises( 183 | UnsupportedParametersError, 184 | match="In WebAssembly environments `parallelism` must be 1.", 185 | ): 186 | ph = PasswordHasher.from_parameters(params) 187 | 188 | # explicitly correct parameters 189 | ph = PasswordHasher(parallelism=1) 190 | 191 | hash = ph.hash("hello") 192 | 193 | assert ph.verify(hash, "hello") is True 194 | 195 | # explicit, but still default parameters 196 | default_params = profiles.get_default_parameters() 197 | ph = PasswordHasher.from_parameters(default_params) 198 | 199 | hash = ph.hash("hello") 200 | 201 | assert ph.verify(hash, "hello") is True 202 | 203 | 204 | def test_multithreaded_hashing(): 205 | """ 206 | Hash passwords in a thread pool and check for thread safety 207 | """ 208 | hasher = PasswordHasher(parallelism=2) 209 | 210 | num_passwords = 100 211 | 212 | passwords = [secrets.token_urlsafe(15) for _ in range(num_passwords)] 213 | 214 | def closure(b, passwords): 215 | b.wait() 216 | for password in passwords: 217 | assert hasher.verify(hasher.hash(password), password) 218 | 219 | max_workers = 4 220 | 221 | chunks = [passwords[i::max_workers] for i in range(max_workers)] 222 | orig_interval = sys.getswitchinterval() 223 | 224 | with ThreadPoolExecutor(max_workers=max_workers) as tpe: 225 | barrier = threading.Barrier(max_workers) 226 | futures = [] 227 | try: 228 | sys.setswitchinterval(0.00001) 229 | for chunk in chunks: 230 | futures.append(tpe.submit(closure, barrier, chunk)) # noqa: PERF401 231 | finally: 232 | sys.setswitchinterval(orig_interval) 233 | if len(futures) < max_workers: 234 | barrier.abort() 235 | for f in futures: 236 | f.result() 237 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | 3 | from base64 import b64encode 4 | from dataclasses import replace 5 | 6 | import pytest 7 | 8 | from hypothesis import given 9 | from hypothesis import strategies as st 10 | 11 | from argon2 import Parameters, Type, extract_parameters 12 | from argon2._utils import NoneType, _check_types, _decoded_str_len 13 | from argon2.exceptions import InvalidHashError 14 | 15 | 16 | class TestCheckTypes: 17 | def test_success(self): 18 | """ 19 | Returns None if all types are okay. 20 | """ 21 | assert None is _check_types( 22 | bytes=(b"bytes", bytes), 23 | tuple=((1, 2), tuple), 24 | str_or_None=(None, (str, NoneType)), 25 | ) 26 | 27 | def test_fail(self): 28 | """ 29 | Returns summary of failures. 30 | """ 31 | rv = _check_types( 32 | bytes=("not bytes", bytes), str_or_None=(42, (str, NoneType)) 33 | ) 34 | 35 | assert "." == rv[-1] # proper grammar FTW 36 | assert "'str_or_None' must be a str, or NoneType (got int)" in rv 37 | 38 | assert "'bytes' must be a bytes (got str)" in rv 39 | 40 | 41 | @given(st.binary()) 42 | def test_decoded_str_len(bs): 43 | """ 44 | _decoded_str_len computes the resulting length. 45 | """ 46 | assert len(bs) == _decoded_str_len(len(b64encode(bs).rstrip(b"="))) 47 | 48 | 49 | VALID_HASH = ( 50 | "$argon2id$v=19$m=65536,t=2,p=4$" 51 | "c29tZXNhbHQ$GpZ3sK/oH9p7VIiV56G/64Zo/8GaUw434IimaPqxwCo" 52 | ) 53 | VALID_PARAMETERS = Parameters( 54 | type=Type.ID, 55 | salt_len=8, 56 | hash_len=32, 57 | version=19, 58 | memory_cost=65536, 59 | time_cost=2, 60 | parallelism=4, 61 | ) 62 | 63 | VALID_HASH_V18 = ( 64 | "$argon2i$m=8,t=1,p=1$c29tZXNhbHQ$gwQOXSNhxiOxPOA0+PY10P9QFO" 65 | "4NAYysnqRt1GSQLE55m+2GYDt9FEjPMHhP2Cuf0nOEXXMocVrsJAtNSsKyfg" 66 | ) 67 | VALID_PARAMETERS_V18 = Parameters( 68 | type=Type.I, 69 | salt_len=8, 70 | hash_len=64, 71 | version=18, 72 | memory_cost=8, 73 | time_cost=1, 74 | parallelism=1, 75 | ) 76 | 77 | 78 | class TestExtractParameters: 79 | def test_valid_hash(self): 80 | """ 81 | A valid hash is parsed. 82 | """ 83 | parsed = extract_parameters(VALID_HASH) 84 | 85 | assert VALID_PARAMETERS == parsed 86 | 87 | def test_valid_hash_v18(self): 88 | """ 89 | A valid Argon v1.2 hash is parsed. 90 | """ 91 | 92 | parsed = extract_parameters(VALID_HASH_V18) 93 | 94 | assert VALID_PARAMETERS_V18 == parsed 95 | 96 | @pytest.mark.parametrize( 97 | "hash", 98 | [ 99 | "", 100 | "abc" + VALID_HASH, 101 | VALID_HASH.replace("p=4", "p=four"), 102 | VALID_HASH.replace(",p=4", ""), 103 | ], 104 | ) 105 | def test_invalid_hash(self, hash): 106 | """ 107 | Invalid hashes of various types raise an InvalidHash error. 108 | """ 109 | with pytest.raises(InvalidHashError): 110 | extract_parameters(hash) 111 | 112 | 113 | class TestParameters: 114 | def test_eq(self): 115 | """ 116 | Parameters are equal iff every attribute is equal. 117 | """ 118 | assert VALID_PARAMETERS == VALID_PARAMETERS # noqa: PLR0124 119 | assert VALID_PARAMETERS != replace(VALID_PARAMETERS, salt_len=9) 120 | 121 | def test_eq_wrong_type(self): 122 | """ 123 | Parameters are only compared if they have the same type. 124 | """ 125 | assert VALID_PARAMETERS != "foo" 126 | assert VALID_PARAMETERS != object() 127 | 128 | def test_repr(self): 129 | """ 130 | __repr__ returns s ensible string. 131 | """ 132 | assert repr( 133 | Parameters( 134 | type=Type.ID, 135 | salt_len=8, 136 | hash_len=32, 137 | version=19, 138 | memory_cost=65536, 139 | time_cost=2, 140 | parallelism=4, 141 | ) 142 | ) == ( 143 | "Parameters(type=, version=19, salt_len=8, " 144 | "hash_len=32, time_cost=2, memory_cost=65536, parallelism=4)" 145 | ) 146 | -------------------------------------------------------------------------------- /tests/typing/api.py: -------------------------------------------------------------------------------- 1 | import argon2 2 | 3 | 4 | argon2.PasswordHasher.from_parameters(argon2.profiles.RFC_9106_HIGH_MEMORY) 5 | ph = argon2.PasswordHasher() 6 | 7 | ph.hash("pw") 8 | ph.hash("pw", salt=b"salt") 9 | ph.hash(b"pw") 10 | ph.hash(b"pw", salt=b"salt") 11 | ph.verify("hash", "pw") 12 | ph.verify(b"hash", "pw") 13 | ph.verify(b"hash", b"pw") 14 | ph.verify("hash", b"pw") 15 | 16 | if ph.check_needs_rehash("hash") is True: 17 | ... 18 | 19 | params: argon2.Parameters = argon2.profiles.get_default_parameters() 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | min_version = 4 3 | env_list = 4 | pre-commit, 5 | mypy-pkg, 6 | py3{8,9,10,11,12,13,14}-{tests,mypy} 7 | py312-bindings-main, 8 | pypy3-tests, 9 | system-argon2, 10 | docs-doctests, 11 | coverage-report 12 | 13 | 14 | [testenv] 15 | description = Run tests / check types and do NOT measure coverage. 16 | package = wheel 17 | wheel_build_env = .pkg 18 | dependency_groups = 19 | tests: tests 20 | mypy: typing 21 | pass_env = 22 | FORCE_COLOR 23 | NO_COLOR 24 | commands = 25 | tests: pytest {posargs} 26 | tests: python -Im argon2 -n 1 -t 1 -m 8 -p 1 27 | mypy: mypy tests/typing 28 | 29 | 30 | [testenv:py3{8,13}-tests] 31 | description = Run tests and measure coverage. 32 | deps = 33 | coverage[toml] 34 | commands = 35 | coverage run -m pytest {posargs} 36 | coverage run -m argon2 -n 1 -t 1 -m 8 -p 1 37 | coverage run -m argon2 --profile CHEAPEST 38 | 39 | 40 | [testenv:coverage-report] 41 | description = Report coverage over all test runs. 42 | skip_install = true 43 | depends = py3{8,13}-tests 44 | deps = coverage[toml] 45 | parallel_show_output = true 46 | commands = 47 | coverage combine 48 | coverage report 49 | 50 | 51 | [testenv:system-argon2] 52 | description = Run tests against bindings that use a system installation of Argon2. 53 | set_env = ARGON2_CFFI_USE_SYSTEM=1 54 | install_command = pip install {opts} --no-binary=argon2-cffi-bindings {packages} 55 | 56 | 57 | [testenv:py312-bindings-main] 58 | description = Run tests against the current main branch of argon2-cffi-bindings 59 | dependency_groups = 60 | deps = 61 | commands_pre = pip install -I hypothesis pytest git+https://github.com/hynek/argon2-cffi-bindings 62 | install_command = pip install {opts} --no-deps {packages} 63 | 64 | 65 | [testenv:pre-commit] 66 | description = Run all pre-commit hooks. 67 | skip_install = true 68 | deps = pre-commit-uv 69 | commands = pre-commit run --all-files 70 | 71 | 72 | [testenv:pyright] 73 | deps = pyright 74 | dependency_groups = typing 75 | commands = pyright tests/typing src 76 | 77 | 78 | [testenv:mypy-pkg] 79 | description = Check own code. 80 | deps = mypy 81 | commands = mypy src 82 | 83 | 84 | [testenv:docs-{build,doctests,linkcheck}] 85 | # Keep base_python in sync with .readthedocs.yaml. 86 | base_python = py313 87 | dependency_groups = docs 88 | commands = 89 | build: sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs {posargs:docs/_build/}html 90 | doctests: python -m doctest README.md 91 | doctests: sphinx-build -n -T -W -b doctest -d {envtmpdir}/doctrees docs {posargs:docs/_build/}html 92 | linkcheck: sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/html 93 | 94 | 95 | [testenv:docs-watch] 96 | package = editable 97 | base_python = {[testenv:docs-build]base_python} 98 | dependency_groups = {[testenv:docs-build]dependency_groups} 99 | deps = watchfiles 100 | commands = 101 | watchfiles \ 102 | --ignore-paths docs/_build/ \ 103 | 'sphinx-build -W -n --jobs auto -b html -d {envtmpdir}/doctrees docs docs/_build/html' \ 104 | src \ 105 | docs \ 106 | README.md \ 107 | CHANGELOG.md 108 | 109 | [testenv:docs-linkcheck] 110 | base_python = {[testenv:docs]base_python} 111 | dependency_groups = {[testenv:docs]dependency_groups} 112 | commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/html 113 | -------------------------------------------------------------------------------- /zizmor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | unpinned-uses: 4 | config: 5 | policies: 6 | # We trust GitHub, the PyPA, and ourselves. 7 | "actions/*": ref-pin 8 | "github/*": ref-pin 9 | "pypa/*": ref-pin 10 | "hynek/*": ref-pin 11 | --------------------------------------------------------------------------------