From dd0b48a459435d3fcc13b2e3854e3666fca1b89d Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 27 Nov 2021 10:15:45 +0100 Subject: [PATCH] Initial --- .github/CODE_OF_CONDUCT.md | 133 +++++++++ .github/CONTRIBUTING.md | 178 ++++++++++++ .github/FUNDING.yml | 3 + .github/workflows/main.yml | 104 +++++++ .github/workflows/wheels.yml | 45 +++ .gitignore | 6 + .gitmodules | 3 + .pre-commit-config.yaml | 30 ++ CHANGELOG.md | 12 + LICENSE | 21 ++ MANIFEST.in | 6 + README.md | 84 ++++++ extras/libargon2 | 1 + pyproject.toml | 22 ++ setup.py | 357 ++++++++++++++++++++++++ src/_argon2_cffi_bindings/__init__.py | 6 + src/_argon2_cffi_bindings/_ffi_build.py | 177 ++++++++++++ tests/test_smoke.py | 19 ++ tox.ini | 79 ++++++ 19 files changed, 1286 insertions(+) create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/wheels.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 160000 extras/libargon2 create mode 100644 pyproject.toml create mode 100644 setup.py create mode 100644 src/_argon2_cffi_bindings/__init__.py create mode 100644 src/_argon2_cffi_bindings/_ffi_build.py create mode 100644 tests/test_smoke.py create mode 100644 tox.ini diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1d8ad18 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..af66fed --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,178 @@ +# How To Contribute + +First off, thank you for considering contributing to *argon2-cffi-bindings*! +It's people like *you* who make it such a great tool for everyone. + +This document intends to make contribution more accessible by codifying tribal knowledge and expectations. +Don't be afraid to open half-finished PRs, and ask questions if something is unclear! + +Please note that this project is released with a Contributor [Code of Conduct](https://github.com/hynek/argon2-cffi-bindings/blob/main/.github/CODE_OF_CONDUCT.md). +By participating in this project you agree to abide by its terms. +Please report any harm to [Hynek Schlawack] in any way you find appropriate. + + +## Workflow + +- No contribution is too small! + Please submit as many fixes for typos and grammar bloopers as you can! +- Try to limit each pull request to *one* change only. +- Since we squash on merge, it's up to you how you handle updates to the main branch. + Whether you prefer to rebase on main or merge main into your branch, do whatever is more comfortable for you. +- *Always* add tests and docs for your code. + This is a hard rule; patches with missing tests or documentation can't be merged. +- Make sure your changes pass our [CI]. + You won't get any feedback until it's green unless you ask for it. +- Once you've addressed review feedback, make sure to bump the pull request with a short note, so we know you're done. +- Don’t break backwards compatibility. + + +## Local Development Environment + +You can (and should) run our test suite using [*tox*]. +However, you’ll probably want a more traditional environment as well. +We highly recommend to develop using the latest Python release because we try to take advantage of modern features whenever possible. + +First create a [virtual environment](https://virtualenv.pypa.io/) so you don't break your system-wide Python installation. +It’s out of scope for this document to list all the ways to manage virtual environments in Python, but if you don’t already have a pet way, take some time to look at tools like [*direnv*](https://hynek.me/til/python-project-local-venvs/), [*virtualfish*](https://virtualfish.readthedocs.io/), and [*virtualenvwrapper*](https://virtualenvwrapper.readthedocs.io/). + +Next, get an up to date checkout of the *argon2-cffi-bindings* repository: + +```console +$ git clone git@github.com:hynek/argon2-cffi-bindings.git +``` + +or if you want to use git via `https`: + +```console +$ git clone https://github.com/hynek/argon2-cffi-bindings.git +``` + +Change into the newly created directory and **activate your virtual environment** + + First you have to make sure that our *Argon2* *git* submodule is up-to-date: + +```console +$ cd argon2-cffi-bindings +$ git submodule init # initialize git submodule mechanics +$ git submodule update # update the vendored Argon2 C library to the version we are packaging +``` + +Now an editable version of *argon2-cffi-bindings* along with its test requirements can be installed as usual: + +```console +$ python -m pip install --upgrade pip setuptools cffi # PLEASE don't skip this step +$ python setup.py build # build the vendored C code +$ python -m pip install -e '.[dev]' +``` + +At this point, + +```console +$ python -m pytest +``` + +should work and pass. + +To avoid committing code that violates our style guide, we strongly advise you to install [*pre-commit*] [^dev] hooks: + +```console +$ pre-commit install +``` + +You can also run them anytime (as our tox does) using: + +```console +$ pre-commit run --all-files +``` + +[^dev]: *pre-commit* should have been installed into your virtualenv automatically when you ran `pip install -e '.[dev]'` above. + If *pre-commit* is missing, your probably need to run `pip install -e '.[dev]'` again. + + +## Code + +- Obey [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 257](https://www.python.org/dev/peps/pep-0257/). + We use the `"""`-on-separate-lines style for docstrings: + + ```python + def func(x): + """ + Do something. + + :param str x: A very important parameter. + + :rtype: str + """ + ``` +- If you add or change public APIs, tag the docstring using `.. versionadded:: 16.0.0 WHAT` or `.. versionchanged:: 16.2.0 WHAT`. +- We use [*isort*](https://github.com/PyCQA/isort) to sort our imports, and we use [*Black*](https://github.com/psf/black) with line length of 79 characters to format our code. + 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) below), you won't have to spend any time on formatting your code at all. + If you don't, [CI] will catch it for you – but that seems like a waste of your time! + + +## Tests + +- Write your asserts as `expected == actual` to line them up nicely: + + ```python + x = f() + + assert 42 == x.some_attribute + assert "foo" == x._a_private_attribute + ``` + +- To run the test suite, all you need is a recent [*tox*]. + It will ensure the test suite runs with all dependencies against all Python versions just as it will in our [CI]. + If you lack some Python versions, you can can always limit the environments like `tox -e py38,py39`, or make it a non-failure using `tox --skip-missing-interpreters`. + + In that case you should look into [*asdf*](https://asdf-vm.com) or [*pyenv*](https://github.com/pyenv/pyenv), which make it very easy to install many different Python versions in parallel. +- Write [good test docstrings](https://jml.io/pages/test-docstrings.html). + + +## Documentation + +- Use [semantic newlines] in [Markdown*] files (files ending in `.md`): + + ```markdown + This is a sentence. + This is another sentence. + ``` + + +### Changelog + +If your change is noteworthy, there needs to be a changelog entry in `CHANGELOG.md`. + +- As with other docs, please use [semantic newlines] in the changelog. +- Wrap symbols like modules, functions, or classes into backticks so they are rendered in a `monospace font`. +- Wrap arguments into asterisks like in docstrings: + `Added new argument *an_argument*.` +- If you mention functions or other callables, add parentheses at the end of their names: + `_argon2_cffi_bindings.func()` or `_argon2_cffi_bindings.Class.method()`. + This makes the changelog a lot more readable. +- Prefer simple past tense or constructions with "now". + For example: + + + Added `_argon2_cffi_bindings.func()`. + + `_argon2_cffi_bindings.func()` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. + +Example entries: + +```markdown +Added `_argon2_cffi_bindings.func()`. +The feature really *is* awesome. +``` + +or: + +```markdown +`_argon2_cffi_bindings.func()` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. +The bug really *was* nasty. +``` + + +[CI]: https://github.com/hynek/argon2-cffi-bindings/actions +[Hynek Schlawack]: https://hynek.me/about/ +[*pre-commit*]: https://pre-commit.com/ +[*tox*]: https://https://tox.wiki/ +[semantic newlines]: https://rhodesmill.org/brandon/2012/one-sentence-per-line/ diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..1405372 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +--- +github: hynek +ko_fi: the_hynek diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..fe758fa --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,104 @@ +--- +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +env: + FORCE_COLOR: "1" # Make tools pretty. + TOX_TESTENV_PASSENV: "FORCE_COLOR" + PYTHON_LATEST: "3.10" + + +jobs: + tests: + name: "tox on ${{ matrix.python-version }}" + runs-on: "ubuntu-latest" + strategy: + matrix: + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7"] + + steps: + - uses: "actions/checkout@v2" + with: + submodules: "recursive" + - uses: "actions/setup-python@v2" + with: + python-version: "${{ matrix.python-version }}" + - name: "Install dependencies" + run: | + python -VV + python -m site + python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade virtualenv tox tox-gh-actions + + - name: "Run tox targets for ${{ matrix.python-version }}" + run: "python -m tox" + + + system-package: + runs-on: "ubuntu-latest" + name: "Install and test with system package of Argon2." + + steps: + - uses: "actions/checkout@v2" + - uses: "actions/setup-python@v2" + with: + python-version: ${{env.PYTHON_LATEST}} + - name: "Install dependencies" + run: | + sudo apt-get install libargon2-0 libargon2-0-dev + # Ensure we cannot use our own Argon2 by accident. + rm -rf extras + python -VV + python -m site + python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade virtualenv tox + + - run: "python -m tox -e system-argon2" + + + package: + name: "Build & verify package" + runs-on: "ubuntu-latest" + + steps: + - uses: "actions/checkout@v2" + with: + submodules: "recursive" + - uses: "actions/setup-python@v2" + with: + python-version: ${{env.PYTHON_LATEST}} + + - run: "python -m pip install build twine check-wheel-contents" + - run: "python -m build --sdist --wheel ." + - run: "ls -l dist" + - run: "check-wheel-contents dist/*.whl" + - name: "Check long_description" + run: "python -m twine check dist/*" + + + install-dev: + name: "Verify dev env" + runs-on: "${{ matrix.os }}" + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "windows-latest", "macos-latest"] + + steps: + - uses: "actions/checkout@v2" + with: + submodules: "recursive" + - uses: "actions/setup-python@v2" + with: + python-version: ${{env.PYTHON_LATEST}} + - run: python -m pip install --upgrade pip + - run: python setup.py build + - run: python -m pip install -e .[dev] + - name: "Import package" + run: "python -c 'from _argon2_cffi_bindings import ffi, lib; print(lib.ARGON2_VERSION_NUMBER)'" diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 0000000..8122cb0 --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,45 @@ +--- +name: Build Wheels + +on: + push: + tags: + - '*' + workflow_dispatch: + + +jobs: + wheels: + name: Building for ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v2 + with: + submodules: "recursive" + + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v1 + with: + platforms: arm64 + + - uses: pypa/cibuildwheel@v2.3.0 + env: + # Only build CPython 3.6, because we have portable abi3 wheels. + # Windows arm64 is only available on 3.9 and later. + CIBW_BUILD: "cp36-* pp37-* pp38-* cp39-win_arm64" + CIBW_ARCHS_LINUX: "auto aarch64" + CIBW_ARCHS_MACOS: "auto" + + # Unfortunately, Argon2 currently doesn't compile on Apple Silicon. + # CIBW_ARCHS_MACOS: auto universal2 + + - uses: actions/upload-artifact@v2 + with: + path: wheelhouse/*.whl + +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf38669 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.tox +*.egg-info +*.so +build +dist +__pycache__ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..74d2e22 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libargon2"] + path = extras/libargon2 + url = https://github.com/P-H-C/phc-winner-argon2.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..421603f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +--- +ci: + autoupdate_schedule: monthly + +repos: + - repo: https://github.com/psf/black + rev: 21.11b1 + hooks: + - id: black + language_version: python3.10 + + - repo: https://github.com/PyCQA/isort + rev: 5.10.1 + hooks: + - id: isort + additional_dependencies: [toml] + + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + language_version: python3.10 + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: debug-statements + - id: check-toml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1ecd41f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +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/). + + +## Unreleased + +### Added + +- Initial import from [*argon2-cffi*](https://github.com/hynek/argon2-cffi). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..263e33c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Hynek Schlawack + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..59d846e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include *.md *.txt *.ini LICENSE .pre-commit-config.yaml pyproject.toml +exclude src/argon2/_ffi.py .gitmodules extras/libargon2/.git *.yml +graft tests +graft .github +recursive-exclude tests *.pyc +graft extras diff --git a/README.md b/README.md new file mode 100644 index 0000000..601824c --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# Python CFFI Bindings for Argon2 + +*argon2-cffi-bindings* provides low-level [*CFFI*](https://cffi.readthedocs.io/) bindings to the [*Argon2*] password hashing algorithm including a vendored version of them. + + +The currently vendored *Argon2* commit ID is [**`f57e61e`**](https://github.com/P-H-C/phc-winner-argon2/commit/f57e61e19229e23c4445b85494dbf7c07de721cb). + + +> If you want to hash passwords in an application, this package is **not** for you. +> Have a look at [*argon2-cffi*] with its high-level abstractions! + +These bindings have been extracted from [*argon2-cffi*] and it remains its main consumer. +However, they may be used by other packages that want to use *Argon2* library without dealing with C-related complexities. + + +## Usage + +The provided *CFFI* bindings are compiled in API mode. +Best effort is given to provide binary wheels for as many platforms as possible. + +--- + +A copy of [*Argon2*] is vendored and used by default, but can be disabled if *argon2-cffi-bindings* is installed using: + +```console +$ env ARGON2_CFFI_USE_SYSTEM=1 \ + python -m pip install --no-binary=argon2-cffi-bindings argon2-cffi-bindings +``` + + +### Python API + +Since this package is intended to be an implementation detail, it uses a private module name. +Therefore you have to import the symbols from `_argon2_cffi_bindings`: + +```python +from _argon2_cffi_bindings import ffi, lib +``` + +Please refer to [*cffi* documentation](https://cffi.readthedocs.io/en/latest/using.html) on how to use the `ffi` and `lib` objects. + +The list of symbols that are provided can be found in the [`_ffi_build.py` file](https://github.com/hynek/argon2-cffi-bindings/blob/main/src/_argon2_cffi_bindings/_ffi_build.py). + +[*Argon2*]: https://github.com/p-h-c/phc-winner-argon2 +[*argon2-cffi*]: https://argon2-cffi.readthedocs.io/ + + +## Project Information + +*argon2-cffi-bindings* is available under the MIT license, available from [PyPI](https://pypi.org/project/argon2-cffi-bindings/), the source code and documentation can be found on [GitHub](https://github.com/hynek/argon2-cffi-bindings). + +*argon2-cffi-bindings* targets Python 3.6 and later, including PyPy3. + + +### Credits & License + +*argon2-cffi-bindings* is written and maintained by [Hynek Schlawack](https://hynek.me/about/). +It is released under the [MIT license](https://github.com/hynek/argon2-cffi/blob/main/LICENSE>). + +The development is kindly supported by [Variomedia AG](https://www.variomedia.de/). + +The authors of *Argon2* were very helpful to get the library to compile on ancient versions of Visual Studio for ancient versions of Python. + +The documentation quotes frequently in verbatim from the *Argon2* [paper](https://www.password-hashing.net/argon2-specs.pdf) to avoid mistakes by rephrasing. + + +#### Vendored Code + +The original *Argon2* repo can be found at . + +Except for the components listed below, the *Argon2* code in this repository is copyright (c) 2015 Daniel Dinu, Dmitry Khovratovich (main authors), Jean-Philippe Aumasson and Samuel Neves, and under [CC0] license. + +The string encoding routines in src/encoding.c are copyright (c) 2015 Thomas Pornin, and under [CC0] license. + +The [*BLAKE2*](https://www.blake2.net) code in ``src/blake2/`` is copyright (c) Samuel Neves, 2013-2015, and under [CC0] license. + +[CC0]: https://creativecommons.org/publicdomain/zero/1.0/ diff --git a/extras/libargon2 b/extras/libargon2 new file mode 160000 index 0000000..f57e61e --- /dev/null +++ b/extras/libargon2 @@ -0,0 +1 @@ +Subproject commit f57e61e19229e23c4445b85494dbf7c07de721cb diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6f0690c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools>=45", "setuptools_scm>=6.2", "wheel", "cffi>=1.0"] +build-backend = "setuptools.build_meta" + + +[tool.setuptools_scm] + +[tool.pytest.ini_options] +addopts = "-ra --strict-markers --capture=no" +xfail_strict = true +testpaths = "tests" +filterwarnings = [ + "once::Warning", +] + + +[tool.black] +line-length = 79 + + +[tool.isort] +profile = "attrs" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2517f2b --- /dev/null +++ b/setup.py @@ -0,0 +1,357 @@ +# SPDX-License-Identifier: MIT + +import os +import platform +import re +import sys + +from distutils.command.build import build +from distutils.command.build_clib import build_clib +from distutils.errors import DistutilsSetupError +from pathlib import Path + +from setuptools import find_packages, setup +from setuptools.command.install import install + + +############################################################################### + +NAME = "argon2-cffi-bindings" +DESCRIPTION = "Low-level CFFI bindings for Argon2" +URL = "https://github.com/hynek/argon2-cffi-bindings" +PACKAGES = find_packages(where="src") +LICENSE = "MIT" +AUTHOR = "Hynek Schlawack" +EMAIL = "hs@ox.cx" + + +HERE = Path(__file__).parent.resolve() + +use_sse2 = os.environ.get("ARGON2_CFFI_USE_SSE2", None) +if use_sse2 == "1": + optimized = True +elif use_sse2 == "0": + optimized = False +else: + # Optimized version requires SSE2 extensions. They have been around since + # 2001 so we try to compile it on every recent-ish x86. + optimized = platform.machine() in ("i686", "x86", "x86_64", "AMD64") + +CFFI_MODULES = ["src/_argon2_cffi_bindings/_ffi_build.py:ffi"] + +lib_base = Path("extras") / "libargon2" / "src" +include_dirs = [ + str(lib_base / path) + for path in ( + Path("..") / "include", + "blake2", + ) +] +sources = [ + str(lib_base / path) + for path in ( + "argon2.c", + Path("blake2") / "blake2b.c", + "core.c", + "encoding.c", + "opt.c" if optimized else "ref.c", + "thread.c", + ) +] +# This is the definition of the vendored libargon2. +LIBRARIES = [("argon2", {"include_dirs": include_dirs, "sources": sources})] + +windows = "win32" in str(sys.platform).lower() + +META_PATH = HERE / "src" / "_argon2_cffi_bindings" / "__init__.py" +KEYWORDS = ["password", "hash", "hashing", "security", "bindings", "cffi"] +PROJECT_URLS = { + "Source Code": "https://github.com/hynek/argon2-cffi-bindings", + "Funding": "https://github.com/sponsors/hynek", + "Tidelift": "https://tidelift.com/subscription/pkg/pypi-argon2-cffi?" + "utm_source=pypi-argon2-cffi&utm_medium=pypi", + "Ko-fi": "https://ko-fi.com/the_hynek", +} +CLASSIFIERS = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: Unix", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python", + "Topic :: Security :: Cryptography", + "Topic :: Security", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +PYTHON_REQUIRES = ">=3.6" +SETUP_REQUIRES = ["cffi>=1.0.0", "setuptools_scm>=6.2"] +INSTALL_REQUIRES = ["cffi>=1.0.0"] +EXTRAS_REQUIRE = {"tests": ["pytest"]} +EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + [ + "cogapp", + "pre-commit", + "wheel", +] + +############################################################################### + + +def keywords_with_side_effects(argv): + """ + Get a dictionary with setup keywords that (can) have side effects. + + :param argv: A list of strings with command line arguments. + + :returns: A dictionary with keyword arguments for the ``setup()`` function. + This setup.py script uses the setuptools 'setup_requires' feature + because this is required by the cffi package to compile extension + modules. The purpose of ``keywords_with_side_effects()`` is to avoid + triggering the cffi build process as a result of setup.py invocations + that don't need the cffi module to be built (setup.py serves the dual + purpose of exposing package metadata). + + Stolen from pyca/cryptography. + """ + no_setup_requires_arguments = ( + "-h", + "--help", + "-n", + "--dry-run", + "-q", + "--quiet", + "-v", + "--verbose", + "-V", + "--version", + "--author", + "--author-email", + "--classifiers", + "--contact", + "--contact-email", + "--description", + "--egg-base", + "--fullname", + "--help-commands", + "--keywords", + "--licence", + "--license", + "--long-description", + "--maintainer", + "--maintainer-email", + "--name", + "--no-user-cfg", + "--obsoletes", + "--platforms", + "--provides", + "--requires", + "--url", + "clean", + "egg_info", + "register", + "sdist", + "upload", + ) + + def is_short_option(argument): + """Check whether a command line argument is a short option.""" + return len(argument) >= 2 and argument[0] == "-" and argument[1] != "-" + + def expand_short_options(argument): + """Expand combined short options into canonical short options.""" + return ("-" + char for char in argument[1:]) + + def argument_without_setup_requirements(argv, i): + """Check whether a command line argument needs setup requirements.""" + if argv[i] in no_setup_requires_arguments: + # Simple case: An argument which is either an option or a command + # which doesn't need setup requirements. + return True + elif is_short_option(argv[i]) and all( + option in no_setup_requires_arguments + for option in expand_short_options(argv[i]) + ): + # Not so simple case: Combined short options none of which need + # setup requirements. + return True + elif argv[i - 1 : i] == ["--egg-base"]: + # Tricky case: --egg-info takes an argument which should not make + # us use setup_requires (defeating the purpose of this code). + return True + else: + return False + + if all( + argument_without_setup_requirements(argv, i) + for i in range(1, len(argv)) + ): + return {"cmdclass": {"build": DummyBuild, "install": DummyInstall}} + else: + global LIBRARIES + use_system_argon2 = ( + os.environ.get("ARGON2_CFFI_USE_SYSTEM", "0") == "1" + ) + if use_system_argon2: + disable_subcommand(build, "build_clib") + LIBRARIES = [] + cmdclass = {"build_clib": BuildCLibWithCompilerFlags} + if BDistWheel is not None: + cmdclass["bdist_wheel"] = BDistWheel + return { + "setup_requires": SETUP_REQUIRES, + "cffi_modules": CFFI_MODULES, + "libraries": LIBRARIES, + "cmdclass": cmdclass, + } + + +def disable_subcommand(command, subcommand_name): + for name, method in command.sub_commands: + if name == subcommand_name: + command.sub_commands.remove((subcommand_name, method)) + break + + +setup_requires_error = ( + "Requested setup command that needs 'setup_requires' while command line " + "arguments implied a side effect free command or option." +) + + +class DummyBuild(build): + """ + This class makes it very obvious when ``keywords_with_side_effects()`` has + incorrectly interpreted the command line arguments to ``setup.py build`` as + one of the 'side effect free' commands or options. + """ + + def run(self): + raise RuntimeError(setup_requires_error) + + +class DummyInstall(install): + """ + This class makes it very obvious when ``keywords_with_side_effects()`` has + incorrectly interpreted the command line arguments to ``setup.py install`` + as one of the 'side effect free' commands or options. + """ + + def run(self): + raise RuntimeError(setup_requires_error) + + +META_FILE = META_PATH.read_text() + + +def find_meta(meta): + """ + Extract __*meta*__ from META_FILE. + """ + meta_match = re.search( + fr"^__{meta}__ = ['\"]([^'\"]*)['\"]", META_FILE, re.M + ) + if meta_match: + return meta_match.group(1) + raise RuntimeError(f"Unable to find __{meta}__ string.") + + +LONG = (HERE / "README.md").read_text() + + +class BuildCLibWithCompilerFlags(build_clib): + """ + We need to pass ``-msse2`` for the optimized build. + """ + + def build_libraries(self, libraries): + """ + Mostly copy pasta from ``distutils.command.build_clib``. + """ + for (lib_name, build_info) in libraries: + sources = build_info.get("sources") + if sources is None or not isinstance(sources, (list, tuple)): + raise DistutilsSetupError( + "in 'libraries' option (library '%s'), " + "'sources' must be present and must be " + "a list of source filenames" % lib_name + ) + sources = list(sources) + + print(f"building '{lib_name}' library") + + # First, compile the source code to object files in the library + # directory. (This should probably change to putting object + # files in a temporary build directory.) + macros = build_info.get("macros") + include_dirs = build_info.get("include_dirs") + objects = self.compiler.compile( + sources, + extra_preargs=["-msse2"] if optimized and not windows else [], + output_dir=self.build_temp, + macros=macros, + include_dirs=include_dirs, + debug=self.debug, + ) + + # Now "link" the object files together into a static library. + # (On Unix at least, this isn't really linking -- it just + # builds an archive. Whatever.) + self.compiler.create_static_lib( + objects, lib_name, output_dir=self.build_clib, debug=self.debug + ) + + +if sys.version_info > (3,) and platform.python_implementation() == "CPython": + try: + import wheel.bdist_wheel + except ImportError: + BDistWheel = None + else: + + class BDistWheel(wheel.bdist_wheel.bdist_wheel): + def finalize_options(self): + self.py_limited_api = f"cp3{sys.version_info[1]}" + wheel.bdist_wheel.bdist_wheel.finalize_options(self) + + +else: + BDistWheel = None + + +if __name__ == "__main__": + setup( + name=NAME, + description=DESCRIPTION, + license=LICENSE, + url=URL, + project_urls=PROJECT_URLS, + use_scm_version=True, # setuptools_scm + author=AUTHOR, + author_email=EMAIL, + maintainer=AUTHOR, + maintainer_email=EMAIL, + long_description=LONG, + long_description_content_type="text/markdown", + keywords=KEYWORDS, + packages=PACKAGES, + package_dir={"": "src"}, + classifiers=CLASSIFIERS, + python_requires=PYTHON_REQUIRES, + install_requires=INSTALL_REQUIRES, + extras_require=EXTRAS_REQUIRE, + # CFFI + zip_safe=False, + ext_package="_argon2_cffi_bindings", + **keywords_with_side_effects(sys.argv), + ) diff --git a/src/_argon2_cffi_bindings/__init__.py b/src/_argon2_cffi_bindings/__init__.py new file mode 100644 index 0000000..8def07a --- /dev/null +++ b/src/_argon2_cffi_bindings/__init__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: MIT + +from ._ffi import ffi, lib + + +__all__ = ["ffi", "lib"] diff --git a/src/_argon2_cffi_bindings/_ffi_build.py b/src/_argon2_cffi_bindings/_ffi_build.py new file mode 100644 index 0000000..2d7bc31 --- /dev/null +++ b/src/_argon2_cffi_bindings/_ffi_build.py @@ -0,0 +1,177 @@ +# SPDX-License-Identifier: MIT + +import os + +from cffi import FFI + + +include_dirs = [os.path.join("extras", "libargon2", "include")] +use_system_argon2 = os.environ.get("ARGON2_CFFI_USE_SYSTEM", "0") == "1" +if use_system_argon2: + include_dirs = [] + + +ffi = FFI() +ffi.set_source( + "_ffi", + "#include ", + include_dirs=include_dirs, + libraries=["argon2"], +) + +ffi.cdef( + """\ +typedef enum Argon2_type { + Argon2_d = ..., + Argon2_i = ..., + Argon2_id = ..., +} argon2_type; +typedef enum Argon2_version { + ARGON2_VERSION_10 = ..., + ARGON2_VERSION_13 = ..., + ARGON2_VERSION_NUMBER = ... +} argon2_version; + +int argon2_hash(const uint32_t t_cost, const uint32_t m_cost, + const uint32_t parallelism, const void *pwd, + const size_t pwdlen, const void *salt, + const size_t saltlen, void *hash, + const size_t hashlen, char *encoded, + const size_t encodedlen, argon2_type type, + const uint32_t version); + +int argon2_verify(const char *encoded, const void *pwd, + const size_t pwdlen, argon2_type type); + +const char *argon2_error_message(int error_code); + + +typedef int (*allocate_fptr)(uint8_t **memory, size_t bytes_to_allocate); +typedef void (*deallocate_fptr)(uint8_t *memory, size_t bytes_to_allocate); + +typedef struct Argon2_Context { + uint8_t *out; /* output array */ + uint32_t outlen; /* digest length */ + + uint8_t *pwd; /* password array */ + uint32_t pwdlen; /* password length */ + + uint8_t *salt; /* salt array */ + uint32_t saltlen; /* salt length */ + + uint8_t *secret; /* key array */ + uint32_t secretlen; /* key length */ + + uint8_t *ad; /* associated data array */ + uint32_t adlen; /* associated data length */ + + uint32_t t_cost; /* number of passes */ + uint32_t m_cost; /* amount of memory requested (KB) */ + uint32_t lanes; /* number of lanes */ + uint32_t threads; /* maximum number of threads */ + + uint32_t version; /* version number */ + + allocate_fptr allocate_cbk; /* pointer to memory allocator */ + deallocate_fptr free_cbk; /* pointer to memory deallocator */ + + uint32_t flags; /* array of bool options */ +} argon2_context; + +int argon2_ctx(argon2_context *context, argon2_type type); + +/* Error codes */ +typedef enum Argon2_ErrorCodes { + ARGON2_OK = ..., + + ARGON2_OUTPUT_PTR_NULL = ..., + + ARGON2_OUTPUT_TOO_SHORT = ..., + ARGON2_OUTPUT_TOO_LONG = ..., + + ARGON2_PWD_TOO_SHORT = ..., + ARGON2_PWD_TOO_LONG = ..., + + ARGON2_SALT_TOO_SHORT = ..., + ARGON2_SALT_TOO_LONG = ..., + + ARGON2_AD_TOO_SHORT = ..., + ARGON2_AD_TOO_LONG = ..., + + ARGON2_SECRET_TOO_SHORT = ..., + ARGON2_SECRET_TOO_LONG = ..., + + ARGON2_TIME_TOO_SMALL = ..., + ARGON2_TIME_TOO_LARGE = ..., + + ARGON2_MEMORY_TOO_LITTLE = ..., + ARGON2_MEMORY_TOO_MUCH = ..., + + ARGON2_LANES_TOO_FEW = ..., + ARGON2_LANES_TOO_MANY = ..., + + ARGON2_PWD_PTR_MISMATCH = ..., /* NULL ptr with non-zero length */ + ARGON2_SALT_PTR_MISMATCH = ..., /* NULL ptr with non-zero length */ + ARGON2_SECRET_PTR_MISMATCH = ..., /* NULL ptr with non-zero length */ + ARGON2_AD_PTR_MISMATCH = ..., /* NULL ptr with non-zero length */ + + ARGON2_MEMORY_ALLOCATION_ERROR = ..., + + ARGON2_FREE_MEMORY_CBK_NULL = ..., + ARGON2_ALLOCATE_MEMORY_CBK_NULL = ..., + + ARGON2_INCORRECT_PARAMETER = ..., + ARGON2_INCORRECT_TYPE = ..., + + ARGON2_OUT_PTR_MISMATCH = ..., + + ARGON2_THREADS_TOO_FEW = ..., + ARGON2_THREADS_TOO_MANY = ..., + + ARGON2_MISSING_ARGS = ..., + + ARGON2_ENCODING_FAIL = ..., + + ARGON2_DECODING_FAIL = ..., + + ARGON2_THREAD_FAIL = ..., + + ARGON2_DECODING_LENGTH_FAIL= ..., + + ARGON2_VERIFY_MISMATCH = ..., +} argon2_error_codes; + +#define ARGON2_FLAG_CLEAR_PASSWORD ... +#define ARGON2_FLAG_CLEAR_SECRET ... +#define ARGON2_DEFAULT_FLAGS ... + +#define ARGON2_MIN_LANES ... +#define ARGON2_MAX_LANES ... +#define ARGON2_MIN_THREADS ... +#define ARGON2_MAX_THREADS ... +#define ARGON2_SYNC_POINTS ... +#define ARGON2_MIN_OUTLEN ... +#define ARGON2_MAX_OUTLEN ... +#define ARGON2_MIN_MEMORY ... +#define ARGON2_MAX_MEMORY_BITS ... +#define ARGON2_MAX_MEMORY ... +#define ARGON2_MIN_TIME ... +#define ARGON2_MAX_TIME ... +#define ARGON2_MIN_PWD_LENGTH ... +#define ARGON2_MAX_PWD_LENGTH ... +#define ARGON2_MIN_AD_LENGTH ... +#define ARGON2_MAX_AD_LENGTH ... +#define ARGON2_MIN_SALT_LENGTH ... +#define ARGON2_MAX_SALT_LENGTH ... +#define ARGON2_MIN_SECRET ... +#define ARGON2_MAX_SECRET ... + +uint32_t argon2_encodedlen(uint32_t t_cost, uint32_t m_cost, + uint32_t parallelism, uint32_t saltlen, + uint32_t hashlen, argon2_type type); + +""" +) + +if __name__ == "__main__": + ffi.compile() diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..7453844 --- /dev/null +++ b/tests/test_smoke.py @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: MIT + +""" +Since this package doesn't do anything beyond providing bindings, all we can +do is trying to ensure that those bindings are functional. +""" + + +def test_smoke(): + """ + lib and ffi can be imported and looks OK. + """ + from _argon2_cffi_bindings import ffi, lib + + assert repr(ffi).startswith("<_cffi_backend.FFI object at") + assert repr(lib).startswith("=3.3.0 +commands = python -m cogapp -rP README.md + + +[testenv:cogCheck] +description = "Ensure README.md is up to date" +skip_install = true +deps = {[testenv:cog]deps} +commands = python -m cogapp --check -P README.md + + +[testenv] +description = Run tests. +extras = tests +commands = + python -m pytest {posargs} + + +[testenv:system-argon2] +description = Run tests against bindings that use a system installation of Argon2. +setenv = ARGON2_CFFI_USE_SYSTEM=1 +extras = tests +install_command = pip install {opts} --no-binary=argon2-cffi-bindings {packages} +commands = + python -m pytest {posargs} + + +[testenv:manifest] +description = Ensure MANIFEST.in is up to date. +deps = check-manifest +skip_install = true +commands = check-manifest + + +[testenv:pypi-description] +description = Ensure README.rst renders on PyPI. +skip_install = true +deps = + twine + pip >= 18.0.0 +commands = + pip wheel -w {envtmpdir}/build --no-deps . + twine check {envtmpdir}/build/*